gobby 0.2.5__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 (383) hide show
  1. gobby/__init__.py +3 -0
  2. gobby/adapters/__init__.py +30 -0
  3. gobby/adapters/base.py +93 -0
  4. gobby/adapters/claude_code.py +276 -0
  5. gobby/adapters/codex.py +1292 -0
  6. gobby/adapters/gemini.py +343 -0
  7. gobby/agents/__init__.py +37 -0
  8. gobby/agents/codex_session.py +120 -0
  9. gobby/agents/constants.py +112 -0
  10. gobby/agents/context.py +362 -0
  11. gobby/agents/definitions.py +133 -0
  12. gobby/agents/gemini_session.py +111 -0
  13. gobby/agents/registry.py +618 -0
  14. gobby/agents/runner.py +968 -0
  15. gobby/agents/session.py +259 -0
  16. gobby/agents/spawn.py +916 -0
  17. gobby/agents/spawners/__init__.py +77 -0
  18. gobby/agents/spawners/base.py +142 -0
  19. gobby/agents/spawners/cross_platform.py +266 -0
  20. gobby/agents/spawners/embedded.py +225 -0
  21. gobby/agents/spawners/headless.py +226 -0
  22. gobby/agents/spawners/linux.py +125 -0
  23. gobby/agents/spawners/macos.py +277 -0
  24. gobby/agents/spawners/windows.py +308 -0
  25. gobby/agents/tty_config.py +319 -0
  26. gobby/autonomous/__init__.py +32 -0
  27. gobby/autonomous/progress_tracker.py +447 -0
  28. gobby/autonomous/stop_registry.py +269 -0
  29. gobby/autonomous/stuck_detector.py +383 -0
  30. gobby/cli/__init__.py +67 -0
  31. gobby/cli/__main__.py +8 -0
  32. gobby/cli/agents.py +529 -0
  33. gobby/cli/artifacts.py +266 -0
  34. gobby/cli/daemon.py +329 -0
  35. gobby/cli/extensions.py +526 -0
  36. gobby/cli/github.py +263 -0
  37. gobby/cli/init.py +53 -0
  38. gobby/cli/install.py +614 -0
  39. gobby/cli/installers/__init__.py +37 -0
  40. gobby/cli/installers/antigravity.py +65 -0
  41. gobby/cli/installers/claude.py +363 -0
  42. gobby/cli/installers/codex.py +192 -0
  43. gobby/cli/installers/gemini.py +294 -0
  44. gobby/cli/installers/git_hooks.py +377 -0
  45. gobby/cli/installers/shared.py +737 -0
  46. gobby/cli/linear.py +250 -0
  47. gobby/cli/mcp.py +30 -0
  48. gobby/cli/mcp_proxy.py +698 -0
  49. gobby/cli/memory.py +304 -0
  50. gobby/cli/merge.py +384 -0
  51. gobby/cli/projects.py +79 -0
  52. gobby/cli/sessions.py +622 -0
  53. gobby/cli/tasks/__init__.py +30 -0
  54. gobby/cli/tasks/_utils.py +658 -0
  55. gobby/cli/tasks/ai.py +1025 -0
  56. gobby/cli/tasks/commits.py +169 -0
  57. gobby/cli/tasks/crud.py +685 -0
  58. gobby/cli/tasks/deps.py +135 -0
  59. gobby/cli/tasks/labels.py +63 -0
  60. gobby/cli/tasks/main.py +273 -0
  61. gobby/cli/tasks/search.py +178 -0
  62. gobby/cli/tui.py +34 -0
  63. gobby/cli/utils.py +513 -0
  64. gobby/cli/workflows.py +927 -0
  65. gobby/cli/worktrees.py +481 -0
  66. gobby/config/__init__.py +129 -0
  67. gobby/config/app.py +551 -0
  68. gobby/config/extensions.py +167 -0
  69. gobby/config/features.py +472 -0
  70. gobby/config/llm_providers.py +98 -0
  71. gobby/config/logging.py +66 -0
  72. gobby/config/mcp.py +346 -0
  73. gobby/config/persistence.py +247 -0
  74. gobby/config/servers.py +141 -0
  75. gobby/config/sessions.py +250 -0
  76. gobby/config/tasks.py +784 -0
  77. gobby/hooks/__init__.py +104 -0
  78. gobby/hooks/artifact_capture.py +213 -0
  79. gobby/hooks/broadcaster.py +243 -0
  80. gobby/hooks/event_handlers.py +723 -0
  81. gobby/hooks/events.py +218 -0
  82. gobby/hooks/git.py +169 -0
  83. gobby/hooks/health_monitor.py +171 -0
  84. gobby/hooks/hook_manager.py +856 -0
  85. gobby/hooks/hook_types.py +575 -0
  86. gobby/hooks/plugins.py +813 -0
  87. gobby/hooks/session_coordinator.py +396 -0
  88. gobby/hooks/verification_runner.py +268 -0
  89. gobby/hooks/webhooks.py +339 -0
  90. gobby/install/claude/commands/gobby/bug.md +51 -0
  91. gobby/install/claude/commands/gobby/chore.md +51 -0
  92. gobby/install/claude/commands/gobby/epic.md +52 -0
  93. gobby/install/claude/commands/gobby/eval.md +235 -0
  94. gobby/install/claude/commands/gobby/feat.md +49 -0
  95. gobby/install/claude/commands/gobby/nit.md +52 -0
  96. gobby/install/claude/commands/gobby/ref.md +52 -0
  97. gobby/install/claude/hooks/HOOK_SCHEMAS.md +632 -0
  98. gobby/install/claude/hooks/hook_dispatcher.py +364 -0
  99. gobby/install/claude/hooks/validate_settings.py +102 -0
  100. gobby/install/claude/hooks-template.json +118 -0
  101. gobby/install/codex/hooks/hook_dispatcher.py +153 -0
  102. gobby/install/codex/prompts/forget.md +7 -0
  103. gobby/install/codex/prompts/memories.md +7 -0
  104. gobby/install/codex/prompts/recall.md +7 -0
  105. gobby/install/codex/prompts/remember.md +13 -0
  106. gobby/install/gemini/hooks/hook_dispatcher.py +268 -0
  107. gobby/install/gemini/hooks-template.json +138 -0
  108. gobby/install/shared/plugins/code_guardian.py +456 -0
  109. gobby/install/shared/plugins/example_notify.py +331 -0
  110. gobby/integrations/__init__.py +10 -0
  111. gobby/integrations/github.py +145 -0
  112. gobby/integrations/linear.py +145 -0
  113. gobby/llm/__init__.py +40 -0
  114. gobby/llm/base.py +120 -0
  115. gobby/llm/claude.py +578 -0
  116. gobby/llm/claude_executor.py +503 -0
  117. gobby/llm/codex.py +322 -0
  118. gobby/llm/codex_executor.py +513 -0
  119. gobby/llm/executor.py +316 -0
  120. gobby/llm/factory.py +34 -0
  121. gobby/llm/gemini.py +258 -0
  122. gobby/llm/gemini_executor.py +339 -0
  123. gobby/llm/litellm.py +287 -0
  124. gobby/llm/litellm_executor.py +303 -0
  125. gobby/llm/resolver.py +499 -0
  126. gobby/llm/service.py +236 -0
  127. gobby/mcp_proxy/__init__.py +29 -0
  128. gobby/mcp_proxy/actions.py +175 -0
  129. gobby/mcp_proxy/daemon_control.py +198 -0
  130. gobby/mcp_proxy/importer.py +436 -0
  131. gobby/mcp_proxy/lazy.py +325 -0
  132. gobby/mcp_proxy/manager.py +798 -0
  133. gobby/mcp_proxy/metrics.py +609 -0
  134. gobby/mcp_proxy/models.py +139 -0
  135. gobby/mcp_proxy/registries.py +215 -0
  136. gobby/mcp_proxy/schema_hash.py +381 -0
  137. gobby/mcp_proxy/semantic_search.py +706 -0
  138. gobby/mcp_proxy/server.py +549 -0
  139. gobby/mcp_proxy/services/__init__.py +0 -0
  140. gobby/mcp_proxy/services/fallback.py +306 -0
  141. gobby/mcp_proxy/services/recommendation.py +224 -0
  142. gobby/mcp_proxy/services/server_mgmt.py +214 -0
  143. gobby/mcp_proxy/services/system.py +72 -0
  144. gobby/mcp_proxy/services/tool_filter.py +231 -0
  145. gobby/mcp_proxy/services/tool_proxy.py +309 -0
  146. gobby/mcp_proxy/stdio.py +565 -0
  147. gobby/mcp_proxy/tools/__init__.py +27 -0
  148. gobby/mcp_proxy/tools/agents.py +1103 -0
  149. gobby/mcp_proxy/tools/artifacts.py +207 -0
  150. gobby/mcp_proxy/tools/hub.py +335 -0
  151. gobby/mcp_proxy/tools/internal.py +337 -0
  152. gobby/mcp_proxy/tools/memory.py +543 -0
  153. gobby/mcp_proxy/tools/merge.py +422 -0
  154. gobby/mcp_proxy/tools/metrics.py +283 -0
  155. gobby/mcp_proxy/tools/orchestration/__init__.py +23 -0
  156. gobby/mcp_proxy/tools/orchestration/cleanup.py +619 -0
  157. gobby/mcp_proxy/tools/orchestration/monitor.py +380 -0
  158. gobby/mcp_proxy/tools/orchestration/orchestrate.py +746 -0
  159. gobby/mcp_proxy/tools/orchestration/review.py +736 -0
  160. gobby/mcp_proxy/tools/orchestration/utils.py +16 -0
  161. gobby/mcp_proxy/tools/session_messages.py +1056 -0
  162. gobby/mcp_proxy/tools/task_dependencies.py +219 -0
  163. gobby/mcp_proxy/tools/task_expansion.py +591 -0
  164. gobby/mcp_proxy/tools/task_github.py +393 -0
  165. gobby/mcp_proxy/tools/task_linear.py +379 -0
  166. gobby/mcp_proxy/tools/task_orchestration.py +77 -0
  167. gobby/mcp_proxy/tools/task_readiness.py +522 -0
  168. gobby/mcp_proxy/tools/task_sync.py +351 -0
  169. gobby/mcp_proxy/tools/task_validation.py +843 -0
  170. gobby/mcp_proxy/tools/tasks/__init__.py +25 -0
  171. gobby/mcp_proxy/tools/tasks/_context.py +112 -0
  172. gobby/mcp_proxy/tools/tasks/_crud.py +516 -0
  173. gobby/mcp_proxy/tools/tasks/_factory.py +176 -0
  174. gobby/mcp_proxy/tools/tasks/_helpers.py +129 -0
  175. gobby/mcp_proxy/tools/tasks/_lifecycle.py +517 -0
  176. gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +301 -0
  177. gobby/mcp_proxy/tools/tasks/_resolution.py +55 -0
  178. gobby/mcp_proxy/tools/tasks/_search.py +215 -0
  179. gobby/mcp_proxy/tools/tasks/_session.py +125 -0
  180. gobby/mcp_proxy/tools/workflows.py +973 -0
  181. gobby/mcp_proxy/tools/worktrees.py +1264 -0
  182. gobby/mcp_proxy/transports/__init__.py +0 -0
  183. gobby/mcp_proxy/transports/base.py +95 -0
  184. gobby/mcp_proxy/transports/factory.py +44 -0
  185. gobby/mcp_proxy/transports/http.py +139 -0
  186. gobby/mcp_proxy/transports/stdio.py +213 -0
  187. gobby/mcp_proxy/transports/websocket.py +136 -0
  188. gobby/memory/backends/__init__.py +116 -0
  189. gobby/memory/backends/mem0.py +408 -0
  190. gobby/memory/backends/memu.py +485 -0
  191. gobby/memory/backends/null.py +111 -0
  192. gobby/memory/backends/openmemory.py +537 -0
  193. gobby/memory/backends/sqlite.py +304 -0
  194. gobby/memory/context.py +87 -0
  195. gobby/memory/manager.py +1001 -0
  196. gobby/memory/protocol.py +451 -0
  197. gobby/memory/search/__init__.py +66 -0
  198. gobby/memory/search/text.py +127 -0
  199. gobby/memory/viz.py +258 -0
  200. gobby/prompts/__init__.py +13 -0
  201. gobby/prompts/defaults/expansion/system.md +119 -0
  202. gobby/prompts/defaults/expansion/user.md +48 -0
  203. gobby/prompts/defaults/external_validation/agent.md +72 -0
  204. gobby/prompts/defaults/external_validation/external.md +63 -0
  205. gobby/prompts/defaults/external_validation/spawn.md +83 -0
  206. gobby/prompts/defaults/external_validation/system.md +6 -0
  207. gobby/prompts/defaults/features/import_mcp.md +22 -0
  208. gobby/prompts/defaults/features/import_mcp_github.md +17 -0
  209. gobby/prompts/defaults/features/import_mcp_search.md +16 -0
  210. gobby/prompts/defaults/features/recommend_tools.md +32 -0
  211. gobby/prompts/defaults/features/recommend_tools_hybrid.md +35 -0
  212. gobby/prompts/defaults/features/recommend_tools_llm.md +30 -0
  213. gobby/prompts/defaults/features/server_description.md +20 -0
  214. gobby/prompts/defaults/features/server_description_system.md +6 -0
  215. gobby/prompts/defaults/features/task_description.md +31 -0
  216. gobby/prompts/defaults/features/task_description_system.md +6 -0
  217. gobby/prompts/defaults/features/tool_summary.md +17 -0
  218. gobby/prompts/defaults/features/tool_summary_system.md +6 -0
  219. gobby/prompts/defaults/research/step.md +58 -0
  220. gobby/prompts/defaults/validation/criteria.md +47 -0
  221. gobby/prompts/defaults/validation/validate.md +38 -0
  222. gobby/prompts/loader.py +346 -0
  223. gobby/prompts/models.py +113 -0
  224. gobby/py.typed +0 -0
  225. gobby/runner.py +488 -0
  226. gobby/search/__init__.py +23 -0
  227. gobby/search/protocol.py +104 -0
  228. gobby/search/tfidf.py +232 -0
  229. gobby/servers/__init__.py +7 -0
  230. gobby/servers/http.py +636 -0
  231. gobby/servers/models.py +31 -0
  232. gobby/servers/routes/__init__.py +23 -0
  233. gobby/servers/routes/admin.py +416 -0
  234. gobby/servers/routes/dependencies.py +118 -0
  235. gobby/servers/routes/mcp/__init__.py +24 -0
  236. gobby/servers/routes/mcp/hooks.py +135 -0
  237. gobby/servers/routes/mcp/plugins.py +121 -0
  238. gobby/servers/routes/mcp/tools.py +1337 -0
  239. gobby/servers/routes/mcp/webhooks.py +159 -0
  240. gobby/servers/routes/sessions.py +582 -0
  241. gobby/servers/websocket.py +766 -0
  242. gobby/sessions/__init__.py +13 -0
  243. gobby/sessions/analyzer.py +322 -0
  244. gobby/sessions/lifecycle.py +240 -0
  245. gobby/sessions/manager.py +563 -0
  246. gobby/sessions/processor.py +225 -0
  247. gobby/sessions/summary.py +532 -0
  248. gobby/sessions/transcripts/__init__.py +41 -0
  249. gobby/sessions/transcripts/base.py +125 -0
  250. gobby/sessions/transcripts/claude.py +386 -0
  251. gobby/sessions/transcripts/codex.py +143 -0
  252. gobby/sessions/transcripts/gemini.py +195 -0
  253. gobby/storage/__init__.py +21 -0
  254. gobby/storage/agents.py +409 -0
  255. gobby/storage/artifact_classifier.py +341 -0
  256. gobby/storage/artifacts.py +285 -0
  257. gobby/storage/compaction.py +67 -0
  258. gobby/storage/database.py +357 -0
  259. gobby/storage/inter_session_messages.py +194 -0
  260. gobby/storage/mcp.py +680 -0
  261. gobby/storage/memories.py +562 -0
  262. gobby/storage/merge_resolutions.py +550 -0
  263. gobby/storage/migrations.py +860 -0
  264. gobby/storage/migrations_legacy.py +1359 -0
  265. gobby/storage/projects.py +166 -0
  266. gobby/storage/session_messages.py +251 -0
  267. gobby/storage/session_tasks.py +97 -0
  268. gobby/storage/sessions.py +817 -0
  269. gobby/storage/task_dependencies.py +223 -0
  270. gobby/storage/tasks/__init__.py +42 -0
  271. gobby/storage/tasks/_aggregates.py +180 -0
  272. gobby/storage/tasks/_crud.py +449 -0
  273. gobby/storage/tasks/_id.py +104 -0
  274. gobby/storage/tasks/_lifecycle.py +311 -0
  275. gobby/storage/tasks/_manager.py +889 -0
  276. gobby/storage/tasks/_models.py +300 -0
  277. gobby/storage/tasks/_ordering.py +119 -0
  278. gobby/storage/tasks/_path_cache.py +110 -0
  279. gobby/storage/tasks/_queries.py +343 -0
  280. gobby/storage/tasks/_search.py +143 -0
  281. gobby/storage/workflow_audit.py +393 -0
  282. gobby/storage/worktrees.py +547 -0
  283. gobby/sync/__init__.py +29 -0
  284. gobby/sync/github.py +333 -0
  285. gobby/sync/linear.py +304 -0
  286. gobby/sync/memories.py +284 -0
  287. gobby/sync/tasks.py +641 -0
  288. gobby/tasks/__init__.py +8 -0
  289. gobby/tasks/build_verification.py +193 -0
  290. gobby/tasks/commits.py +633 -0
  291. gobby/tasks/context.py +747 -0
  292. gobby/tasks/criteria.py +342 -0
  293. gobby/tasks/enhanced_validator.py +226 -0
  294. gobby/tasks/escalation.py +263 -0
  295. gobby/tasks/expansion.py +626 -0
  296. gobby/tasks/external_validator.py +764 -0
  297. gobby/tasks/issue_extraction.py +171 -0
  298. gobby/tasks/prompts/expand.py +327 -0
  299. gobby/tasks/research.py +421 -0
  300. gobby/tasks/tdd.py +352 -0
  301. gobby/tasks/tree_builder.py +263 -0
  302. gobby/tasks/validation.py +712 -0
  303. gobby/tasks/validation_history.py +357 -0
  304. gobby/tasks/validation_models.py +89 -0
  305. gobby/tools/__init__.py +0 -0
  306. gobby/tools/summarizer.py +170 -0
  307. gobby/tui/__init__.py +5 -0
  308. gobby/tui/api_client.py +281 -0
  309. gobby/tui/app.py +327 -0
  310. gobby/tui/screens/__init__.py +25 -0
  311. gobby/tui/screens/agents.py +333 -0
  312. gobby/tui/screens/chat.py +450 -0
  313. gobby/tui/screens/dashboard.py +377 -0
  314. gobby/tui/screens/memory.py +305 -0
  315. gobby/tui/screens/metrics.py +231 -0
  316. gobby/tui/screens/orchestrator.py +904 -0
  317. gobby/tui/screens/sessions.py +412 -0
  318. gobby/tui/screens/tasks.py +442 -0
  319. gobby/tui/screens/workflows.py +289 -0
  320. gobby/tui/screens/worktrees.py +174 -0
  321. gobby/tui/widgets/__init__.py +21 -0
  322. gobby/tui/widgets/chat.py +210 -0
  323. gobby/tui/widgets/conductor.py +104 -0
  324. gobby/tui/widgets/menu.py +132 -0
  325. gobby/tui/widgets/message_panel.py +160 -0
  326. gobby/tui/widgets/review_gate.py +224 -0
  327. gobby/tui/widgets/task_tree.py +99 -0
  328. gobby/tui/widgets/token_budget.py +166 -0
  329. gobby/tui/ws_client.py +258 -0
  330. gobby/utils/__init__.py +3 -0
  331. gobby/utils/daemon_client.py +235 -0
  332. gobby/utils/git.py +222 -0
  333. gobby/utils/id.py +38 -0
  334. gobby/utils/json_helpers.py +161 -0
  335. gobby/utils/logging.py +376 -0
  336. gobby/utils/machine_id.py +135 -0
  337. gobby/utils/metrics.py +589 -0
  338. gobby/utils/project_context.py +182 -0
  339. gobby/utils/project_init.py +263 -0
  340. gobby/utils/status.py +256 -0
  341. gobby/utils/validation.py +80 -0
  342. gobby/utils/version.py +23 -0
  343. gobby/workflows/__init__.py +4 -0
  344. gobby/workflows/actions.py +1310 -0
  345. gobby/workflows/approval_flow.py +138 -0
  346. gobby/workflows/artifact_actions.py +103 -0
  347. gobby/workflows/audit_helpers.py +110 -0
  348. gobby/workflows/autonomous_actions.py +286 -0
  349. gobby/workflows/context_actions.py +394 -0
  350. gobby/workflows/definitions.py +130 -0
  351. gobby/workflows/detection_helpers.py +208 -0
  352. gobby/workflows/engine.py +485 -0
  353. gobby/workflows/evaluator.py +669 -0
  354. gobby/workflows/git_utils.py +96 -0
  355. gobby/workflows/hooks.py +169 -0
  356. gobby/workflows/lifecycle_evaluator.py +613 -0
  357. gobby/workflows/llm_actions.py +70 -0
  358. gobby/workflows/loader.py +333 -0
  359. gobby/workflows/mcp_actions.py +60 -0
  360. gobby/workflows/memory_actions.py +272 -0
  361. gobby/workflows/premature_stop.py +164 -0
  362. gobby/workflows/session_actions.py +139 -0
  363. gobby/workflows/state_actions.py +123 -0
  364. gobby/workflows/state_manager.py +104 -0
  365. gobby/workflows/stop_signal_actions.py +163 -0
  366. gobby/workflows/summary_actions.py +344 -0
  367. gobby/workflows/task_actions.py +249 -0
  368. gobby/workflows/task_enforcement_actions.py +901 -0
  369. gobby/workflows/templates.py +52 -0
  370. gobby/workflows/todo_actions.py +84 -0
  371. gobby/workflows/webhook.py +223 -0
  372. gobby/workflows/webhook_executor.py +399 -0
  373. gobby/worktrees/__init__.py +5 -0
  374. gobby/worktrees/git.py +690 -0
  375. gobby/worktrees/merge/__init__.py +20 -0
  376. gobby/worktrees/merge/conflict_parser.py +177 -0
  377. gobby/worktrees/merge/resolver.py +485 -0
  378. gobby-0.2.5.dist-info/METADATA +351 -0
  379. gobby-0.2.5.dist-info/RECORD +383 -0
  380. gobby-0.2.5.dist-info/WHEEL +5 -0
  381. gobby-0.2.5.dist-info/entry_points.txt +2 -0
  382. gobby-0.2.5.dist-info/licenses/LICENSE.md +193 -0
  383. gobby-0.2.5.dist-info/top_level.txt +1 -0
@@ -0,0 +1,214 @@
1
+ """Server management service."""
2
+
3
+ import logging
4
+ from typing import TYPE_CHECKING, Any
5
+
6
+ from gobby.mcp_proxy.manager import MCPClientManager
7
+ from gobby.mcp_proxy.models import MCPServerConfig
8
+
9
+ if TYPE_CHECKING:
10
+ from gobby.config.app import DaemonConfig
11
+
12
+ logger = logging.getLogger("gobby.mcp.server")
13
+
14
+
15
+ class ServerManagementService:
16
+ """Service for managing MCP server configurations."""
17
+
18
+ def __init__(
19
+ self,
20
+ mcp_manager: MCPClientManager,
21
+ config_manager: Any,
22
+ config: "DaemonConfig | None" = None,
23
+ ):
24
+ """
25
+ Args:
26
+ mcp_manager: MCP client manager
27
+ config_manager: Config manager (for saving changes)
28
+ config: Daemon configuration (for import operations)
29
+ """
30
+ self._mcp_manager = mcp_manager
31
+ self._config_manager = config_manager
32
+ self._config = config
33
+
34
+ async def add_server(
35
+ self,
36
+ name: str,
37
+ transport: str,
38
+ url: str | None = None,
39
+ command: str | None = None,
40
+ args: list[str] | None = None,
41
+ env: dict[str, str] | None = None,
42
+ headers: dict[str, str] | None = None,
43
+ enabled: bool = True,
44
+ project_id: str | None = None,
45
+ ) -> dict[str, Any]:
46
+ """Add a new MCP server."""
47
+ try:
48
+ # Resolve project ID
49
+ if not project_id:
50
+ from gobby.utils.project_context import get_project_context
51
+
52
+ ctx = get_project_context()
53
+ if ctx and ctx.get("id"):
54
+ project_id = ctx["id"]
55
+
56
+ if not project_id:
57
+ return {
58
+ "success": False,
59
+ "error": "project_id is required. Run 'gobby init' or provide project_id.",
60
+ }
61
+
62
+ # Create config object
63
+ server_config = MCPServerConfig(
64
+ name=name,
65
+ project_id=project_id,
66
+ transport=transport,
67
+ url=url,
68
+ command=command,
69
+ args=args,
70
+ env=env,
71
+ headers=headers,
72
+ enabled=enabled,
73
+ )
74
+ # Validate - catch validation errors separately for clear error messages
75
+ try:
76
+ server_config.validate()
77
+ except ValueError as e:
78
+ return {"success": False, "error": f"Validation error: {e}"}
79
+
80
+ # Add to manager (runtime)
81
+ self._mcp_manager.add_server_config(server_config)
82
+
83
+ # Persist to config
84
+ # self._config_manager.add_mcp_server(...) # Mocking this interaction
85
+
86
+ # Attempt connection
87
+ if enabled:
88
+ try:
89
+ await self._mcp_manager.connect_all([server_config])
90
+ except Exception as e:
91
+ logger.warning(f"Added server {name} but connection failed: {e}")
92
+ return {
93
+ "success": True,
94
+ "message": f"Server added but connection failed: {str(e)}",
95
+ "connected": False,
96
+ }
97
+
98
+ return {
99
+ "success": True,
100
+ "message": f"Server {name} added successfully",
101
+ "connected": enabled,
102
+ }
103
+
104
+ except Exception as e:
105
+ logger.exception(f"Unexpected error adding server {name}")
106
+ return {"success": False, "error": str(e)}
107
+
108
+ async def remove_server(self, name: str) -> dict[str, Any]:
109
+ """Remove an MCP server.
110
+
111
+ Disconnects the server first if connected, then removes the configuration.
112
+ """
113
+ try:
114
+ # First disconnect if connected
115
+ if name in self._mcp_manager._connections:
116
+ try:
117
+ connection = self._mcp_manager._connections[name]
118
+ if connection.is_connected:
119
+ await connection.disconnect()
120
+ # Update health state
121
+ if name in self._mcp_manager.health:
122
+ from gobby.mcp_proxy.models import ConnectionState
123
+
124
+ self._mcp_manager.health[name].state = ConnectionState.DISCONNECTED
125
+ # Remove from connections
126
+ del self._mcp_manager._connections[name]
127
+ logger.info(f"Disconnected server {name} before removal")
128
+ except Exception as e:
129
+ logger.warning(f"Error disconnecting server {name}: {e}")
130
+ # Continue with removal even if disconnect fails
131
+
132
+ # Remove from runtime config
133
+ self._mcp_manager.remove_server_config(name)
134
+
135
+ # Persist
136
+ # self._config_manager.remove_mcp_server(name)
137
+
138
+ return {"success": True, "message": f"Server {name} removed"}
139
+ except Exception as e:
140
+ logger.error(f"Failed to remove server {name}: {e}")
141
+ return {"success": False, "error": str(e)}
142
+
143
+ async def import_server(
144
+ self,
145
+ from_project: str | None = None,
146
+ github_url: str | None = None,
147
+ query: str | None = None,
148
+ servers: list[str] | None = None,
149
+ ) -> dict[str, Any]:
150
+ """Import MCP server(s) from various sources.
151
+
152
+ Args:
153
+ from_project: Import from another Gobby project by name or ID
154
+ github_url: Import from a GitHub repository URL
155
+ query: Import by natural language search query
156
+ servers: Optional list of specific server names to import
157
+
158
+ Returns:
159
+ Result dict with imported servers or error
160
+ """
161
+ # Validate at least one source is provided
162
+ if not from_project and not github_url and not query:
163
+ return {
164
+ "success": False,
165
+ "error": "Specify at least one: from_project, github_url, or query",
166
+ }
167
+
168
+ # Get current project context
169
+ from gobby.utils.project_context import get_project_context
170
+
171
+ project_ctx = get_project_context()
172
+ if not project_ctx or not project_ctx.get("id"):
173
+ return {
174
+ "success": False,
175
+ "error": "No current project. Run 'gobby init' first.",
176
+ }
177
+ current_project_id = project_ctx["id"]
178
+
179
+ # Validate config is available
180
+ if not self._config:
181
+ return {
182
+ "success": False,
183
+ "error": "Daemon configuration not available for import operations",
184
+ }
185
+
186
+ try:
187
+ # Create importer lazily with required dependencies
188
+ from gobby.mcp_proxy.importer import MCPServerImporter
189
+ from gobby.storage.database import LocalDatabase
190
+
191
+ db = LocalDatabase()
192
+ importer = MCPServerImporter(
193
+ config=self._config,
194
+ db=db,
195
+ current_project_id=current_project_id,
196
+ mcp_client_manager=self._mcp_manager,
197
+ )
198
+
199
+ # Execute import based on source
200
+ if from_project:
201
+ return await importer.import_from_project(
202
+ source_project=from_project,
203
+ servers=servers,
204
+ )
205
+ elif github_url:
206
+ return await importer.import_from_github(github_url)
207
+ elif query:
208
+ return await importer.import_from_query(query)
209
+ else:
210
+ return {"success": False, "error": "No import source specified"}
211
+
212
+ except Exception as e:
213
+ logger.exception("Failed to import MCP server")
214
+ return {"success": False, "error": str(e)}
@@ -0,0 +1,72 @@
1
+ """System status service."""
2
+
3
+ import logging
4
+ import os
5
+ from typing import Any
6
+
7
+ from gobby.mcp_proxy.manager import MCPClientManager
8
+
9
+ logger = logging.getLogger("gobby.mcp.server")
10
+
11
+
12
+ class SystemService:
13
+ """Service for system status and information."""
14
+
15
+ def __init__(
16
+ self, mcp_manager: MCPClientManager, port: int, websocket_port: int, start_time: float
17
+ ):
18
+ self._mcp_manager = mcp_manager
19
+ self._port = port
20
+ self._websocket_port = websocket_port
21
+ self._start_time = start_time
22
+
23
+ def get_status(self) -> dict[str, Any]:
24
+ """Get system status."""
25
+ health = self._mcp_manager.get_server_health()
26
+ lazy_states = self._mcp_manager.get_lazy_connection_states()
27
+
28
+ # Merge lazy connection info into health
29
+ for name, lazy_info in lazy_states.items():
30
+ if name in health:
31
+ health[name]["lazy_connection"] = lazy_info
32
+ else:
33
+ # Server registered but never connected (lazy mode)
34
+ health[name] = {
35
+ "state": "configured", # Not connected yet
36
+ "health": "unknown",
37
+ "last_check": None,
38
+ "failures": 0,
39
+ "response_time_ms": None,
40
+ "lazy_connection": lazy_info,
41
+ }
42
+
43
+ # Aggregate health: system is healthy if connected servers are healthy
44
+ # In lazy mode, unconfigured servers don't count as unhealthy
45
+ all_healthy = (
46
+ all(
47
+ server_health.get("state") in ["connected", "healthy", "configured"]
48
+ for server_health in health.values()
49
+ )
50
+ if health
51
+ else True
52
+ )
53
+
54
+ # Count configured vs connected
55
+ configured_count = len(health)
56
+ connected_count = sum(
57
+ 1
58
+ for h in health.values()
59
+ if h.get("state") == "connected" or h.get("lazy_connection", {}).get("is_connected")
60
+ )
61
+
62
+ return {
63
+ "running": True,
64
+ "pid": os.getpid(),
65
+ "healthy": all_healthy,
66
+ "http_port": self._port,
67
+ "websocket_port": self._websocket_port,
68
+ "mcp_servers": health,
69
+ "lazy_mode": self._mcp_manager.lazy_connect,
70
+ "configured_servers": configured_count,
71
+ "connected_servers": connected_count,
72
+ }
@@ -0,0 +1,231 @@
1
+ """Tool filtering service based on workflow step restrictions."""
2
+
3
+ import logging
4
+ from pathlib import Path
5
+ from typing import TYPE_CHECKING, Any
6
+
7
+ if TYPE_CHECKING:
8
+ from gobby.storage.database import LocalDatabase
9
+ from gobby.workflows.loader import WorkflowLoader
10
+ from gobby.workflows.state_manager import WorkflowStateManager
11
+
12
+ logger = logging.getLogger("gobby.mcp.tool_filter")
13
+
14
+
15
+ class ToolFilterService:
16
+ """
17
+ Service to filter tools based on workflow step restrictions.
18
+
19
+ When a session has an active step-based workflow, this service
20
+ filters the tool list to only include tools allowed in the current step.
21
+ """
22
+
23
+ def __init__(
24
+ self,
25
+ db: "LocalDatabase | None" = None,
26
+ loader: "WorkflowLoader | None" = None,
27
+ state_manager: "WorkflowStateManager | None" = None,
28
+ ):
29
+ self._db = db
30
+ self._loader = loader
31
+ self._state_manager = state_manager
32
+
33
+ def _get_state_manager(self) -> "WorkflowStateManager | None":
34
+ """Lazy initialization of state manager."""
35
+ if self._state_manager:
36
+ return self._state_manager
37
+
38
+ if self._db:
39
+ from gobby.workflows.state_manager import WorkflowStateManager
40
+
41
+ self._state_manager = WorkflowStateManager(self._db)
42
+ return self._state_manager
43
+
44
+ return None
45
+
46
+ def _get_loader(self) -> "WorkflowLoader | None":
47
+ """Lazy initialization of workflow loader."""
48
+ if self._loader:
49
+ return self._loader
50
+
51
+ from gobby.workflows.loader import WorkflowLoader
52
+
53
+ self._loader = WorkflowLoader()
54
+ return self._loader
55
+
56
+ def get_step_restrictions(
57
+ self,
58
+ session_id: str,
59
+ project_path: str | Path | None = None,
60
+ ) -> dict[str, Any] | None:
61
+ """
62
+ Get tool restrictions for the current workflow step.
63
+
64
+ Args:
65
+ session_id: Session ID to check
66
+ project_path: Optional project path for loading workflow
67
+
68
+ Returns:
69
+ Dict with allowed_tools and blocked_tools, or None if no workflow active
70
+ """
71
+ state_manager = self._get_state_manager()
72
+ if not state_manager:
73
+ logger.debug("No state manager available for tool filtering")
74
+ return None
75
+
76
+ state = state_manager.get_state(session_id)
77
+ if not state:
78
+ logger.debug(f"No workflow state for session {session_id}")
79
+ return None
80
+
81
+ loader = self._get_loader()
82
+ if not loader:
83
+ logger.debug("No workflow loader available")
84
+ return None
85
+
86
+ proj = Path(project_path) if project_path else None
87
+ definition = loader.load_workflow(state.workflow_name, proj)
88
+ if not definition:
89
+ logger.warning(f"Workflow '{state.workflow_name}' not found")
90
+ return None
91
+
92
+ step = definition.get_step(state.step)
93
+ if not step:
94
+ logger.warning(f"Step '{state.step}' not found in workflow '{state.workflow_name}'")
95
+ return None
96
+
97
+ return {
98
+ "workflow_name": state.workflow_name,
99
+ "step": state.step,
100
+ "allowed_tools": step.allowed_tools,
101
+ "blocked_tools": step.blocked_tools,
102
+ }
103
+
104
+ def is_tool_allowed(
105
+ self,
106
+ tool_name: str,
107
+ session_id: str,
108
+ project_path: str | Path | None = None,
109
+ ) -> tuple[bool, str | None]:
110
+ """
111
+ Check if a tool is allowed in the current workflow step.
112
+
113
+ Args:
114
+ tool_name: Name of the tool to check
115
+ session_id: Session ID
116
+ project_path: Optional project path
117
+
118
+ Returns:
119
+ Tuple of (is_allowed, reason). If no workflow is active, returns (True, None).
120
+ """
121
+ restrictions = self.get_step_restrictions(session_id, project_path)
122
+ if not restrictions:
123
+ return True, None
124
+
125
+ # Check blocked list first
126
+ if tool_name in restrictions["blocked_tools"]:
127
+ return False, f"Tool '{tool_name}' is blocked in step '{restrictions['step']}'"
128
+
129
+ # Check allowed list
130
+ allowed = restrictions["allowed_tools"]
131
+ if allowed == "all":
132
+ return True, None
133
+
134
+ if tool_name not in allowed:
135
+ return (
136
+ False,
137
+ f"Tool '{tool_name}' is not in allowed list for step '{restrictions['step']}'",
138
+ )
139
+
140
+ return True, None
141
+
142
+ def filter_tools(
143
+ self,
144
+ tools: list[dict[str, Any]],
145
+ session_id: str | None = None,
146
+ project_path: str | Path | None = None,
147
+ ) -> list[dict[str, Any]]:
148
+ """
149
+ Filter a list of tools based on workflow step restrictions.
150
+
151
+ Args:
152
+ tools: List of tool dicts with at least a 'name' key
153
+ session_id: Session ID to check for workflow state
154
+ project_path: Optional project path
155
+
156
+ Returns:
157
+ Filtered list of tools. If no session_id or no active workflow,
158
+ returns the original list unchanged.
159
+ """
160
+ if not session_id:
161
+ return tools
162
+
163
+ restrictions = self.get_step_restrictions(session_id, project_path)
164
+ if not restrictions:
165
+ return tools
166
+
167
+ allowed = restrictions["allowed_tools"]
168
+ blocked = restrictions["blocked_tools"]
169
+
170
+ filtered = []
171
+ for tool in tools:
172
+ name = tool.get("name", "")
173
+
174
+ # Skip blocked tools
175
+ if name in blocked:
176
+ logger.debug(f"Filtering out blocked tool: {name}")
177
+ continue
178
+
179
+ # Check allowed list
180
+ if allowed != "all" and name not in allowed:
181
+ logger.debug(f"Filtering out non-allowed tool: {name}")
182
+ continue
183
+
184
+ filtered.append(tool)
185
+
186
+ if len(filtered) < len(tools):
187
+ logger.info(
188
+ f"Filtered {len(tools) - len(filtered)} tools based on step '{restrictions['step']}'"
189
+ )
190
+
191
+ return filtered
192
+
193
+ def filter_servers_tools(
194
+ self,
195
+ servers: list[dict[str, Any]],
196
+ session_id: str | None = None,
197
+ project_path: str | Path | None = None,
198
+ ) -> list[dict[str, Any]]:
199
+ """
200
+ Filter tools from multiple servers based on workflow step restrictions.
201
+
202
+ Args:
203
+ servers: List of server dicts with 'name' and 'tools' keys
204
+ session_id: Session ID to check for workflow state
205
+ project_path: Optional project path
206
+
207
+ Returns:
208
+ Servers list with filtered tools. Empty servers are kept but with empty tool lists.
209
+ """
210
+ if not session_id:
211
+ return servers
212
+
213
+ restrictions = self.get_step_restrictions(session_id, project_path)
214
+ if not restrictions:
215
+ return servers
216
+
217
+ result = []
218
+ for server in servers:
219
+ server_name = server.get("name", "")
220
+ tools = server.get("tools", [])
221
+
222
+ filtered_tools = self.filter_tools(tools, session_id, project_path)
223
+
224
+ result.append(
225
+ {
226
+ "name": server_name,
227
+ "tools": filtered_tools,
228
+ }
229
+ )
230
+
231
+ return result