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,325 @@
1
+ """
2
+ Lazy server initialization with circuit breaker pattern.
3
+
4
+ Provides deferred MCP server connections to reduce startup time and resource usage.
5
+ Servers are connected on-demand when first accessed, with automatic retry and
6
+ circuit breaker protection against cascading failures.
7
+ """
8
+
9
+ import asyncio
10
+ import logging
11
+ import time
12
+ from dataclasses import dataclass, field
13
+ from datetime import UTC, datetime
14
+ from enum import Enum
15
+ from typing import TYPE_CHECKING, Any
16
+
17
+ if TYPE_CHECKING:
18
+ pass
19
+
20
+ logger = logging.getLogger("gobby.mcp.lazy")
21
+
22
+
23
+ class CircuitState(str, Enum):
24
+ """Circuit breaker states."""
25
+
26
+ CLOSED = "closed" # Normal operation, requests pass through
27
+ OPEN = "open" # Circuit tripped, fail fast
28
+ HALF_OPEN = "half_open" # Testing if service recovered
29
+
30
+
31
+ @dataclass
32
+ class CircuitBreaker:
33
+ """
34
+ Circuit breaker for connection protection.
35
+
36
+ Prevents cascading failures by failing fast when a service is down.
37
+
38
+ States:
39
+ - CLOSED: Normal operation, all requests pass through
40
+ - OPEN: Service is down, fail immediately without trying
41
+ - HALF_OPEN: Service may have recovered, allow one test request
42
+ """
43
+
44
+ failure_threshold: int = 3 # Failures before opening circuit
45
+ recovery_timeout: float = 30.0 # Seconds before trying half-open
46
+ half_open_max_calls: int = 1 # Calls allowed in half-open state
47
+
48
+ state: CircuitState = CircuitState.CLOSED
49
+ failure_count: int = 0
50
+ last_failure_time: float | None = None
51
+ half_open_calls: int = 0
52
+
53
+ def record_success(self) -> None:
54
+ """Record successful operation."""
55
+ self.failure_count = 0
56
+ self.half_open_calls = 0
57
+ self.state = CircuitState.CLOSED
58
+
59
+ def record_failure(self) -> None:
60
+ """Record failed operation and potentially trip circuit."""
61
+ self.failure_count += 1
62
+ self.last_failure_time = time.time()
63
+
64
+ if self.state == CircuitState.HALF_OPEN:
65
+ # Failed during recovery test, reopen circuit
66
+ self.state = CircuitState.OPEN
67
+ self.half_open_calls = 0
68
+ logger.warning("Circuit breaker reopened after half-open failure")
69
+ elif self.failure_count >= self.failure_threshold:
70
+ self.state = CircuitState.OPEN
71
+ logger.warning(f"Circuit breaker opened after {self.failure_count} failures")
72
+
73
+ def can_execute(self) -> bool:
74
+ """Check if request can proceed."""
75
+ if self.state == CircuitState.CLOSED:
76
+ return True
77
+
78
+ if self.state == CircuitState.OPEN:
79
+ # Check if recovery timeout has passed
80
+ if self.last_failure_time is None:
81
+ return True
82
+
83
+ elapsed = time.time() - self.last_failure_time
84
+ if elapsed >= self.recovery_timeout:
85
+ self.state = CircuitState.HALF_OPEN
86
+ self.half_open_calls = 0
87
+ logger.info("Circuit breaker entering half-open state")
88
+ return True
89
+ return False
90
+
91
+ if self.state == CircuitState.HALF_OPEN:
92
+ if self.half_open_calls < self.half_open_max_calls:
93
+ self.half_open_calls += 1
94
+ return True
95
+ return False
96
+
97
+ return False
98
+
99
+
100
+ @dataclass
101
+ class RetryConfig:
102
+ """Configuration for exponential backoff retry."""
103
+
104
+ max_retries: int = 3
105
+ initial_delay: float = 1.0 # seconds
106
+ max_delay: float = 16.0 # seconds
107
+ multiplier: float = 2.0
108
+
109
+ def get_delay(self, attempt: int) -> float:
110
+ """Get delay for given attempt number (0-indexed)."""
111
+ delay = self.initial_delay * (self.multiplier**attempt)
112
+ return min(delay, self.max_delay)
113
+
114
+
115
+ @dataclass
116
+ class LazyConnectionState:
117
+ """
118
+ State tracking for a lazy-connected server.
119
+
120
+ Tracks whether a server has been connected, its circuit breaker state,
121
+ and connection timing information.
122
+ """
123
+
124
+ configured_at: datetime = field(default_factory=lambda: datetime.now(UTC))
125
+ connected_at: datetime | None = None
126
+ last_attempt_at: datetime | None = None
127
+ last_error: str | None = None
128
+ connection_attempts: int = 0
129
+ circuit_breaker: CircuitBreaker = field(default_factory=CircuitBreaker)
130
+
131
+ @property
132
+ def is_connected(self) -> bool:
133
+ """Check if server has been successfully connected."""
134
+ return self.connected_at is not None
135
+
136
+ def record_connection_attempt(self) -> None:
137
+ """Record that a connection attempt is starting."""
138
+ self.last_attempt_at = datetime.now(UTC)
139
+ self.connection_attempts += 1
140
+
141
+ def record_connection_success(self) -> None:
142
+ """Record successful connection."""
143
+ self.connected_at = datetime.now(UTC)
144
+ self.last_error = None
145
+ self.circuit_breaker.record_success()
146
+
147
+ def record_connection_failure(self, error: str) -> None:
148
+ """Record failed connection."""
149
+ self.last_error = error
150
+ self.circuit_breaker.record_failure()
151
+
152
+
153
+ class LazyServerConnector:
154
+ """
155
+ Manages lazy initialization of MCP server connections.
156
+
157
+ Instead of connecting all servers at startup, connections are deferred
158
+ until the first tool call or explicit request. This reduces startup time
159
+ and avoids consuming resources for unused servers.
160
+
161
+ Features:
162
+ - Deferred connection on first use
163
+ - Exponential backoff retry on connection failure
164
+ - Circuit breaker to prevent cascading failures
165
+ - Connection state tracking and reporting
166
+ """
167
+
168
+ def __init__(
169
+ self,
170
+ retry_config: RetryConfig | None = None,
171
+ circuit_breaker_config: dict[str, Any] | None = None,
172
+ ):
173
+ """
174
+ Initialize lazy connector.
175
+
176
+ Args:
177
+ retry_config: Retry configuration for connection attempts
178
+ circuit_breaker_config: Circuit breaker settings (failure_threshold,
179
+ recovery_timeout, half_open_max_calls)
180
+ """
181
+ self.retry_config = retry_config or RetryConfig()
182
+ self._circuit_breaker_config = circuit_breaker_config or {}
183
+ self._states: dict[str, LazyConnectionState] = {}
184
+ self._connection_locks: dict[str, asyncio.Lock] = {}
185
+
186
+ def register_server(self, server_name: str) -> None:
187
+ """
188
+ Register a server for lazy connection.
189
+
190
+ Called when a server is configured but not yet connected.
191
+
192
+ Args:
193
+ server_name: Name of the server to register
194
+ """
195
+ if server_name not in self._states:
196
+ self._states[server_name] = LazyConnectionState(
197
+ circuit_breaker=CircuitBreaker(**self._circuit_breaker_config)
198
+ )
199
+ self._connection_locks[server_name] = asyncio.Lock()
200
+ logger.debug(f"Registered server '{server_name}' for lazy connection")
201
+
202
+ def unregister_server(self, server_name: str) -> None:
203
+ """
204
+ Remove a server from lazy connection tracking.
205
+
206
+ Args:
207
+ server_name: Name of the server to unregister
208
+ """
209
+ self._states.pop(server_name, None)
210
+ self._connection_locks.pop(server_name, None)
211
+
212
+ def get_state(self, server_name: str) -> LazyConnectionState | None:
213
+ """
214
+ Get connection state for a server.
215
+
216
+ Args:
217
+ server_name: Name of the server
218
+
219
+ Returns:
220
+ LazyConnectionState or None if not registered
221
+ """
222
+ return self._states.get(server_name)
223
+
224
+ def is_connected(self, server_name: str) -> bool:
225
+ """
226
+ Check if a server is connected.
227
+
228
+ Args:
229
+ server_name: Name of the server
230
+
231
+ Returns:
232
+ True if connected, False otherwise
233
+ """
234
+ state = self._states.get(server_name)
235
+ return state.is_connected if state else False
236
+
237
+ def can_attempt_connection(self, server_name: str) -> bool:
238
+ """
239
+ Check if connection attempt is allowed (circuit breaker not open).
240
+
241
+ Args:
242
+ server_name: Name of the server
243
+
244
+ Returns:
245
+ True if connection can be attempted
246
+ """
247
+ state = self._states.get(server_name)
248
+ if not state:
249
+ return True # Unknown server, allow attempt
250
+ return state.circuit_breaker.can_execute()
251
+
252
+ def mark_connected(self, server_name: str) -> None:
253
+ """
254
+ Mark a server as successfully connected.
255
+
256
+ Args:
257
+ server_name: Name of the server
258
+ """
259
+ state = self._states.get(server_name)
260
+ if state:
261
+ state.record_connection_success()
262
+ logger.info(f"Server '{server_name}' connected")
263
+
264
+ def mark_failed(self, server_name: str, error: str) -> None:
265
+ """
266
+ Mark a server connection as failed.
267
+
268
+ Args:
269
+ server_name: Name of the server
270
+ error: Error message
271
+ """
272
+ state = self._states.get(server_name)
273
+ if state:
274
+ state.record_connection_failure(error)
275
+ logger.warning(f"Server '{server_name}' connection failed: {error}")
276
+
277
+ def get_connection_lock(self, server_name: str) -> asyncio.Lock:
278
+ """
279
+ Get lock for serializing connection attempts to a server.
280
+
281
+ Prevents multiple concurrent connection attempts to the same server.
282
+
283
+ Args:
284
+ server_name: Name of the server
285
+
286
+ Returns:
287
+ asyncio.Lock for the server
288
+ """
289
+ if server_name not in self._connection_locks:
290
+ self._connection_locks[server_name] = asyncio.Lock()
291
+ return self._connection_locks[server_name]
292
+
293
+ def get_all_states(self) -> dict[str, dict[str, Any]]:
294
+ """
295
+ Get connection states for all registered servers.
296
+
297
+ Returns:
298
+ Dict mapping server names to state information
299
+ """
300
+ return {
301
+ name: {
302
+ "is_connected": state.is_connected,
303
+ "configured_at": state.configured_at.isoformat(),
304
+ "connected_at": state.connected_at.isoformat() if state.connected_at else None,
305
+ "last_attempt_at": (
306
+ state.last_attempt_at.isoformat() if state.last_attempt_at else None
307
+ ),
308
+ "last_error": state.last_error,
309
+ "connection_attempts": state.connection_attempts,
310
+ "circuit_state": state.circuit_breaker.state.value,
311
+ "circuit_failures": state.circuit_breaker.failure_count,
312
+ }
313
+ for name, state in self._states.items()
314
+ }
315
+
316
+
317
+ class CircuitBreakerOpen(Exception):
318
+ """Raised when circuit breaker prevents connection attempt."""
319
+
320
+ def __init__(self, server_name: str, recovery_in: float):
321
+ self.server_name = server_name
322
+ self.recovery_in = recovery_in
323
+ super().__init__(
324
+ f"Circuit breaker open for '{server_name}'. Recovery attempt in {recovery_in:.1f}s"
325
+ )