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,436 @@
1
+ """MCP server import functionality."""
2
+
3
+ import logging
4
+ import re
5
+ from typing import TYPE_CHECKING, Any
6
+
7
+ from gobby.config.app import DaemonConfig
8
+ from gobby.storage.database import DatabaseProtocol
9
+ from gobby.storage.mcp import LocalMCPManager
10
+ from gobby.storage.projects import LocalProjectManager
11
+ from gobby.utils.json_helpers import extract_json_object
12
+
13
+ if TYPE_CHECKING:
14
+ from gobby.mcp_proxy.manager import MCPClientManager
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+ # Pattern to detect placeholder secrets like <YOUR_API_KEY>
19
+ SECRET_PLACEHOLDER_PATTERN = re.compile(r"<YOUR_[A-Z0-9_]+>")
20
+
21
+
22
+ class MCPServerImporter:
23
+ """Handles importing MCP servers from various sources."""
24
+
25
+ def __init__(
26
+ self,
27
+ config: DaemonConfig,
28
+ db: DatabaseProtocol,
29
+ current_project_id: str,
30
+ mcp_client_manager: "MCPClientManager | None" = None,
31
+ ):
32
+ """
33
+ Initialize the importer.
34
+
35
+ Args:
36
+ config: Daemon configuration
37
+ db: Database connection
38
+ current_project_id: ID of the current project to import into
39
+ mcp_client_manager: Optional MCP client manager for live connections
40
+ """
41
+ self.config = config
42
+ self.db = db
43
+ self.current_project_id = current_project_id
44
+ self.mcp_db_manager = LocalMCPManager(db)
45
+ self.project_manager = LocalProjectManager(db)
46
+ self.mcp_client_manager = mcp_client_manager
47
+ self.import_config = config.get_import_mcp_server_config()
48
+
49
+ async def import_from_project(
50
+ self,
51
+ source_project: str,
52
+ servers: list[str] | None = None,
53
+ ) -> dict[str, Any]:
54
+ """
55
+ Import MCP servers from another Gobby project.
56
+
57
+ Args:
58
+ source_project: Source project name or ID
59
+ servers: Optional list of server names to import (imports all if None)
60
+
61
+ Returns:
62
+ Result dict with imported servers or error
63
+ """
64
+ # Resolve source project - try by name first, then by ID
65
+ project = self.project_manager.get_by_name(source_project)
66
+ if not project:
67
+ project = self.project_manager.get(source_project)
68
+
69
+ if not project:
70
+ # List available projects for helpful error message
71
+ available = self.project_manager.list()
72
+ project_names = [p.name for p in available]
73
+ return {
74
+ "success": False,
75
+ "error": f"Project '{source_project}' not found",
76
+ "available_projects": project_names,
77
+ }
78
+
79
+ # Get servers from source project
80
+ source_servers = self.mcp_db_manager.list_servers(
81
+ project_id=project.id,
82
+ enabled_only=False, # Include disabled servers too
83
+ )
84
+
85
+ if not source_servers:
86
+ return {
87
+ "success": False,
88
+ "error": f"No MCP servers found in project '{project.name}'",
89
+ }
90
+
91
+ # Filter by server names if specified
92
+ if servers:
93
+ servers_lower = [s.lower() for s in servers]
94
+ source_servers = [s for s in source_servers if s.name.lower() in servers_lower]
95
+ if not source_servers:
96
+ return {
97
+ "success": False,
98
+ "error": f"None of the specified servers found in project '{project.name}'",
99
+ "requested": servers,
100
+ }
101
+
102
+ # Get existing servers in current project to skip duplicates
103
+ existing_servers = self.mcp_db_manager.list_servers(
104
+ project_id=self.current_project_id,
105
+ enabled_only=False,
106
+ )
107
+ existing_names = {s.name.lower() for s in existing_servers}
108
+
109
+ # Import each server
110
+ imported = []
111
+ skipped = []
112
+ failed = []
113
+
114
+ for server in source_servers:
115
+ if server.name.lower() in existing_names:
116
+ skipped.append(server.name)
117
+ continue
118
+
119
+ # Add server using action (connects and saves) or just save to db
120
+ add_result = await self._add_server(
121
+ name=server.name,
122
+ transport=server.transport,
123
+ url=server.url,
124
+ command=server.command,
125
+ args=server.args,
126
+ env=server.env,
127
+ headers=server.headers,
128
+ enabled=server.enabled,
129
+ description=server.description,
130
+ )
131
+
132
+ if add_result.get("success"):
133
+ imported.append(server.name)
134
+ else:
135
+ failed.append({"name": server.name, "error": add_result.get("error")})
136
+
137
+ result: dict[str, Any] = {
138
+ "success": len(imported) > 0 or len(failed) == 0,
139
+ "imported": imported,
140
+ "message": f"Imported {len(imported)} server(s) from project '{project.name}'",
141
+ }
142
+
143
+ if skipped:
144
+ result["skipped"] = skipped
145
+ result["message"] += f" (skipped {len(skipped)} existing)"
146
+
147
+ if failed:
148
+ result["failed"] = failed
149
+
150
+ return result
151
+
152
+ async def import_from_github(self, github_url: str) -> dict[str, Any]:
153
+ """
154
+ Import MCP server from GitHub repository.
155
+
156
+ Uses Claude Agent SDK to fetch and parse the README.
157
+
158
+ Args:
159
+ github_url: GitHub repository URL
160
+
161
+ Returns:
162
+ Result dict with config (may need user input for secrets)
163
+ """
164
+ if not self.import_config.enabled:
165
+ return {
166
+ "success": False,
167
+ "error": "MCP server import is disabled in configuration",
168
+ }
169
+
170
+ try:
171
+ from claude_agent_sdk import AssistantMessage, ClaudeAgentOptions, TextBlock, query
172
+
173
+ # Build prompt to fetch and extract config
174
+ prompt = self.import_config.github_fetch_prompt.format(github_url=github_url)
175
+
176
+ options = ClaudeAgentOptions(
177
+ system_prompt=self.import_config.prompt,
178
+ max_turns=3,
179
+ model=self.import_config.model,
180
+ allowed_tools=["WebFetch"],
181
+ permission_mode="default",
182
+ )
183
+
184
+ # Run query
185
+ result_text = ""
186
+ async for message in query(prompt=prompt, options=options):
187
+ if isinstance(message, AssistantMessage):
188
+ for block in message.content:
189
+ if isinstance(block, TextBlock):
190
+ result_text += block.text
191
+
192
+ # Parse and add if no secrets needed
193
+ return await self._parse_and_add_config(result_text)
194
+
195
+ except Exception as e:
196
+ logger.error(f"Failed to import from GitHub: {e}")
197
+ return {
198
+ "success": False,
199
+ "error": str(e),
200
+ "error_type": type(e).__name__,
201
+ }
202
+
203
+ async def import_from_query(self, search_query: str) -> dict[str, Any]:
204
+ """
205
+ Import MCP server by searching for it.
206
+
207
+ Uses Claude Agent SDK to search and extract configuration.
208
+
209
+ Args:
210
+ search_query: Natural language search query
211
+
212
+ Returns:
213
+ Result dict with config (may need user input for secrets)
214
+ """
215
+ if not self.import_config.enabled:
216
+ return {
217
+ "success": False,
218
+ "error": "MCP server import is disabled in configuration",
219
+ }
220
+
221
+ try:
222
+ from claude_agent_sdk import AssistantMessage, ClaudeAgentOptions, TextBlock, query
223
+
224
+ # Build prompt to search and extract config
225
+ prompt = self.import_config.search_fetch_prompt.format(search_query=search_query)
226
+
227
+ options = ClaudeAgentOptions(
228
+ system_prompt=self.import_config.prompt,
229
+ max_turns=5, # More turns for search + fetch
230
+ model=self.import_config.model,
231
+ allowed_tools=["WebSearch", "WebFetch"],
232
+ permission_mode="default",
233
+ )
234
+
235
+ # Run query
236
+ result_text = ""
237
+ async for message in query(prompt=prompt, options=options):
238
+ if isinstance(message, AssistantMessage):
239
+ for block in message.content:
240
+ if isinstance(block, TextBlock):
241
+ result_text += block.text
242
+
243
+ # Parse and add if no secrets needed
244
+ return await self._parse_and_add_config(result_text)
245
+
246
+ except Exception as e:
247
+ logger.error(f"Failed to import from query: {e}")
248
+ return {
249
+ "success": False,
250
+ "error": str(e),
251
+ "error_type": type(e).__name__,
252
+ }
253
+
254
+ async def _add_server(
255
+ self,
256
+ name: str,
257
+ transport: str,
258
+ url: str | None = None,
259
+ command: str | None = None,
260
+ args: list[str] | None = None,
261
+ env: dict[str, str] | None = None,
262
+ headers: dict[str, str] | None = None,
263
+ enabled: bool = True,
264
+ description: str | None = None,
265
+ ) -> dict[str, Any]:
266
+ """
267
+ Add an MCP server using the action (connects + saves) or db-only fallback.
268
+
269
+ Args:
270
+ name: Server name
271
+ transport: Transport type
272
+ url: Server URL (for http/websocket)
273
+ command: Command (for stdio)
274
+ args: Command args (for stdio)
275
+ env: Environment variables
276
+ headers: HTTP headers
277
+ enabled: Whether server is enabled
278
+ description: Server description
279
+
280
+ Returns:
281
+ Result dict with success status
282
+ """
283
+ try:
284
+ if self.mcp_client_manager:
285
+ # Use the action which connects and saves
286
+ from gobby.mcp_proxy.actions import add_mcp_server
287
+
288
+ result: dict[str, Any] = await add_mcp_server(
289
+ mcp_manager=self.mcp_client_manager,
290
+ name=name,
291
+ transport=transport,
292
+ project_id=self.current_project_id,
293
+ url=url,
294
+ headers=headers,
295
+ command=command,
296
+ args=args,
297
+ env=env,
298
+ enabled=enabled,
299
+ description=description,
300
+ )
301
+ return result
302
+ else:
303
+ # Fallback to db-only (won't be connected until restart)
304
+ self.mcp_db_manager.upsert(
305
+ name=name,
306
+ transport=transport,
307
+ project_id=self.current_project_id,
308
+ url=url,
309
+ command=command,
310
+ args=args,
311
+ env=env,
312
+ headers=headers,
313
+ enabled=enabled,
314
+ description=description,
315
+ )
316
+ return {
317
+ "success": True,
318
+ "imported": [name],
319
+ "message": f"Successfully added MCP server '{name}' (restart daemon to connect)",
320
+ }
321
+
322
+ except Exception as e:
323
+ logger.error(f"Failed to add server '{name}': {e}")
324
+ return {
325
+ "success": False,
326
+ "name": name,
327
+ "error": str(e),
328
+ "error_type": type(e).__name__,
329
+ }
330
+
331
+ async def _parse_and_add_config(self, result_text: str) -> dict[str, Any]:
332
+ """
333
+ Parse LLM response and add server if no secrets needed.
334
+
335
+ Args:
336
+ result_text: Raw text from LLM
337
+
338
+ Returns:
339
+ Success result if added, or needs_configuration if secrets required
340
+ """
341
+ # Try to extract JSON from the response
342
+ config = self._extract_json(result_text)
343
+
344
+ if not config:
345
+ return {
346
+ "success": False,
347
+ "error": "Could not extract valid configuration from documentation",
348
+ "raw_response": result_text[:1000], # Include first 1000 chars for debugging
349
+ }
350
+
351
+ # Check for missing secrets
352
+ missing = self._find_missing_secrets(config)
353
+ instructions = config.pop("instructions", None)
354
+
355
+ if missing:
356
+ # Secrets needed - return config for user to fill in
357
+ result: dict[str, Any] = {
358
+ "status": "needs_configuration",
359
+ "config": config,
360
+ "missing": missing,
361
+ }
362
+ if instructions:
363
+ result["instructions"] = instructions
364
+ return result
365
+
366
+ # No secrets needed - add the server directly
367
+ name = config.get("name")
368
+ transport = config.get("transport")
369
+
370
+ if not name or not transport:
371
+ return {
372
+ "success": False,
373
+ "error": "Extracted config missing required fields: name or transport",
374
+ "config": config,
375
+ }
376
+
377
+ return await self._add_server(
378
+ name=name,
379
+ transport=transport,
380
+ url=config.get("url"),
381
+ command=config.get("command"),
382
+ args=config.get("args"),
383
+ env=config.get("env"),
384
+ headers=config.get("headers"),
385
+ enabled=config.get("enabled", True),
386
+ description=config.get("description"),
387
+ )
388
+
389
+ def _extract_json(self, text: str) -> dict[str, Any] | None:
390
+ """
391
+ Extract JSON object from text.
392
+
393
+ Handles JSON in code blocks or raw JSON.
394
+
395
+ Args:
396
+ text: Text potentially containing JSON
397
+
398
+ Returns:
399
+ Parsed JSON dict or None
400
+ """
401
+ result = extract_json_object(text)
402
+ if result is None:
403
+ return None
404
+
405
+ # Validate it looks like a server config
406
+ if "name" in result or "transport" in result:
407
+ return result
408
+
409
+ return None
410
+
411
+ def _find_missing_secrets(self, config: dict[str, Any]) -> list[str]:
412
+ """
413
+ Find placeholder secrets in config.
414
+
415
+ Args:
416
+ config: Server configuration dict
417
+
418
+ Returns:
419
+ List of placeholder secret names
420
+ """
421
+ missing = []
422
+
423
+ def check_value(value: Any, path: str = "") -> None:
424
+ if isinstance(value, str):
425
+ match = SECRET_PLACEHOLDER_PATTERN.search(value)
426
+ if match:
427
+ missing.append(match.group(0))
428
+ elif isinstance(value, dict):
429
+ for k, v in value.items():
430
+ check_value(v, f"{path}.{k}" if path else k)
431
+ elif isinstance(value, list):
432
+ for i, v in enumerate(value):
433
+ check_value(v, f"{path}[{i}]")
434
+
435
+ check_value(config)
436
+ return missing