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
gobby/cli/mcp_proxy.py ADDED
@@ -0,0 +1,698 @@
1
+ """
2
+ MCP proxy CLI commands.
3
+
4
+ Provides CLI access to MCP proxy functionality:
5
+ - list-servers: List configured MCP servers
6
+ - list-tools: List tools from MCP servers
7
+ - get-schema: Get full schema for a specific tool
8
+ - call-tool: Execute a tool on an MCP server
9
+ - add-server: Add a new MCP server configuration
10
+ - remove-server: Remove an MCP server configuration
11
+ - recommend-tools: Get AI-powered tool recommendations
12
+ """
13
+
14
+ import json
15
+ import sys
16
+ import urllib.parse
17
+ from typing import Any, cast
18
+
19
+ import click
20
+
21
+ from gobby.config.app import DaemonConfig
22
+ from gobby.utils.daemon_client import DaemonClient
23
+
24
+
25
+ def get_daemon_client(ctx: click.Context) -> DaemonClient:
26
+ """Get daemon client from context config."""
27
+ config: DaemonConfig = ctx.obj["config"]
28
+ return DaemonClient(host="localhost", port=config.daemon_port)
29
+
30
+
31
+ def check_daemon_running(client: DaemonClient) -> bool:
32
+ """Check if daemon is running and print error if not."""
33
+ is_healthy, error = client.check_health()
34
+ if not is_healthy:
35
+ if error is None:
36
+ click.echo("Error: Gobby daemon is not running. Start it with: gobby start", err=True)
37
+ else:
38
+ click.echo(f"Error: Cannot connect to daemon: {error}", err=True)
39
+ return False
40
+ return True
41
+
42
+
43
+ def call_mcp_api(
44
+ client: DaemonClient,
45
+ endpoint: str,
46
+ method: str = "POST",
47
+ json_data: dict[str, Any] | None = None,
48
+ timeout: float = 30.0,
49
+ ) -> dict[str, Any] | None:
50
+ """Call MCP API endpoint and handle errors."""
51
+ try:
52
+ response = client.call_http_api(
53
+ endpoint, method=method, json_data=json_data, timeout=timeout
54
+ )
55
+ if response.status_code == 200:
56
+ return cast(dict[str, Any], response.json())
57
+ else:
58
+ error_msg = response.text or f"HTTP {response.status_code}"
59
+ click.echo(f"Error: {error_msg}", err=True)
60
+ return None
61
+ except Exception as e:
62
+ click.echo(f"Error: {e}", err=True)
63
+ return None
64
+
65
+
66
+ @click.group("mcp-proxy")
67
+ def mcp_proxy() -> None:
68
+ """Manage MCP proxy servers and tools."""
69
+ pass
70
+
71
+
72
+ @mcp_proxy.command("list-servers")
73
+ @click.option("--json", "json_format", is_flag=True, help="Output as JSON")
74
+ @click.pass_context
75
+ def list_servers(ctx: click.Context, json_format: bool) -> None:
76
+ """List all configured MCP servers."""
77
+ client = get_daemon_client(ctx)
78
+ if not check_daemon_running(client):
79
+ sys.exit(1)
80
+
81
+ result = call_mcp_api(client, "/mcp/servers", method="GET")
82
+ if result is None:
83
+ sys.exit(1)
84
+
85
+ servers = result.get("servers", [])
86
+
87
+ if json_format:
88
+ click.echo(json.dumps(result, indent=2))
89
+ return
90
+
91
+ if not servers:
92
+ click.echo("No MCP servers configured.")
93
+ return
94
+
95
+ connected = result.get("connected_count", 0)
96
+ total = result.get("total_count", 0)
97
+ click.echo(f"MCP Servers ({connected}/{total} connected):")
98
+ for server in servers:
99
+ status_icon = "●" if server.get("connected") else "○"
100
+ state = server.get("state", "unknown")
101
+ click.echo(f" {status_icon} {server['name']} ({state})")
102
+
103
+
104
+ @mcp_proxy.command("list-tools")
105
+ @click.option("--server", "-s", help="Filter by server name")
106
+ @click.option("--json", "json_format", is_flag=True, help="Output as JSON")
107
+ @click.pass_context
108
+ def list_tools(ctx: click.Context, server: str | None, json_format: bool) -> None:
109
+ """List tools from MCP servers."""
110
+ client = get_daemon_client(ctx)
111
+ if not check_daemon_running(client):
112
+ sys.exit(1)
113
+
114
+ endpoint = "/mcp/tools"
115
+ if server:
116
+ encoded_server = urllib.parse.quote(server)
117
+ endpoint = f"/mcp/tools?server_filter={encoded_server}"
118
+
119
+ result = call_mcp_api(client, endpoint, method="GET")
120
+ if result is None:
121
+ sys.exit(1)
122
+
123
+ if json_format:
124
+ click.echo(json.dumps(result, indent=2))
125
+ return
126
+
127
+ tools_by_server = result.get("tools", {})
128
+ if not tools_by_server:
129
+ click.echo("No tools available.")
130
+ return
131
+
132
+ for server_name, tools in tools_by_server.items():
133
+ click.echo(f"\n{server_name}:")
134
+ if not tools:
135
+ click.echo(" (no tools)")
136
+ continue
137
+ for tool in tools:
138
+ name = tool.get("name", "unknown")
139
+ brief = tool.get("brief", tool.get("description", ""))[:60]
140
+ click.echo(f" • {name}")
141
+ if brief:
142
+ click.echo(f" {brief}")
143
+
144
+
145
+ @mcp_proxy.command("get-schema")
146
+ @click.argument("server_name")
147
+ @click.argument("tool_name")
148
+ @click.pass_context
149
+ def get_schema(ctx: click.Context, server_name: str, tool_name: str) -> None:
150
+ """Get full schema for a specific tool.
151
+
152
+ Examples:
153
+ gobby mcp-proxy get-schema context7 get-library-docs
154
+ gobby mcp-proxy get-schema supabase list_tables
155
+ """
156
+ client = get_daemon_client(ctx)
157
+ if not check_daemon_running(client):
158
+ sys.exit(1)
159
+
160
+ result = call_mcp_api(
161
+ client,
162
+ "/mcp/tools/schema",
163
+ method="POST",
164
+ json_data={"server_name": server_name, "tool_name": tool_name},
165
+ )
166
+ if result is None:
167
+ sys.exit(1)
168
+
169
+ # Always output as JSON for schema (it's complex)
170
+ click.echo(json.dumps(result, indent=2))
171
+
172
+
173
+ @mcp_proxy.command("call-tool")
174
+ @click.argument("server_name")
175
+ @click.argument("tool_name")
176
+ @click.option("--arg", "-a", "args", multiple=True, help="Tool argument in key=value format")
177
+ @click.option("--json-args", "-j", "json_args", help="Tool arguments as JSON string")
178
+ @click.option("--raw", is_flag=True, help="Output raw result without formatting")
179
+ @click.pass_context
180
+ def call_tool(
181
+ ctx: click.Context,
182
+ server_name: str,
183
+ tool_name: str,
184
+ args: tuple[str, ...],
185
+ json_args: str | None,
186
+ raw: bool,
187
+ ) -> None:
188
+ """Execute a tool on an MCP server.
189
+
190
+ Examples:
191
+ gobby mcp-proxy call-tool supabase list_tables
192
+ gobby mcp-proxy call-tool context7 get-library-docs -a topic=react -a tokens=5000
193
+ gobby mcp-proxy call-tool myserver mytool -j '{"key": "value"}'
194
+ """
195
+ client = get_daemon_client(ctx)
196
+ if not check_daemon_running(client):
197
+ sys.exit(1)
198
+
199
+ # Parse arguments
200
+ arguments: dict[str, Any] = {}
201
+
202
+ if json_args:
203
+ try:
204
+ arguments = json.loads(json_args)
205
+ except json.JSONDecodeError as e:
206
+ click.echo(f"Error: Invalid JSON arguments: {e}", err=True)
207
+ sys.exit(1)
208
+
209
+ # Add key=value args (override JSON args)
210
+ for arg in args:
211
+ if "=" not in arg:
212
+ click.echo(f"Error: Invalid argument format '{arg}'. Use key=value", err=True)
213
+ sys.exit(1)
214
+ key, value = arg.split("=", 1)
215
+ # Try to parse value as JSON for proper typing
216
+ try:
217
+ arguments[key] = json.loads(value)
218
+ except json.JSONDecodeError:
219
+ arguments[key] = value
220
+
221
+ result = call_mcp_api(
222
+ client,
223
+ "/mcp/tools/call",
224
+ method="POST",
225
+ json_data={
226
+ "server_name": server_name,
227
+ "tool_name": tool_name,
228
+ "arguments": arguments,
229
+ },
230
+ )
231
+ if result is None:
232
+ sys.exit(1)
233
+
234
+ if raw:
235
+ click.echo(json.dumps(result, indent=2))
236
+ else:
237
+ # Format result nicely
238
+ if result.get("success"):
239
+ content = result.get("result", result)
240
+ if isinstance(content, dict):
241
+ click.echo(json.dumps(content, indent=2))
242
+ else:
243
+ click.echo(content)
244
+ else:
245
+ click.echo(f"Error: {result.get('error', 'Unknown error')}", err=True)
246
+ sys.exit(1)
247
+
248
+
249
+ @mcp_proxy.command("add-server")
250
+ @click.argument("name")
251
+ @click.option("--transport", "-t", required=True, type=click.Choice(["http", "stdio", "websocket"]))
252
+ @click.option("--url", "-u", help="Server URL (for http/websocket)")
253
+ @click.option("--command", "-c", help="Command to run (for stdio)")
254
+ @click.option("--args", "-A", "cmd_args", help="Command arguments as JSON array (for stdio)")
255
+ @click.option("--env", "-e", help="Environment variables as JSON object")
256
+ @click.option("--headers", help="HTTP headers as JSON object")
257
+ @click.option("--disabled", is_flag=True, help="Add server as disabled")
258
+ @click.pass_context
259
+ def add_server(
260
+ ctx: click.Context,
261
+ name: str,
262
+ transport: str,
263
+ url: str | None,
264
+ command: str | None,
265
+ cmd_args: str | None,
266
+ env: str | None,
267
+ headers: str | None,
268
+ disabled: bool,
269
+ ) -> None:
270
+ """Add a new MCP server configuration.
271
+
272
+ Examples:
273
+ gobby mcp-proxy add-server my-http -t http -u https://api.example.com/mcp
274
+ gobby mcp-proxy add-server my-stdio -t stdio -c npx --args '["mcp-server"]'
275
+ """
276
+ client = get_daemon_client(ctx)
277
+ if not check_daemon_running(client):
278
+ sys.exit(1)
279
+
280
+ # Validate transport requirements
281
+ if transport in ("http", "websocket") and not url:
282
+ click.echo(f"Error: --url is required for {transport} transport", err=True)
283
+ sys.exit(1)
284
+ if transport == "stdio" and not command:
285
+ click.echo("Error: --command is required for stdio transport", err=True)
286
+ sys.exit(1)
287
+
288
+ # Parse JSON options
289
+ parsed_args = None
290
+ parsed_env = None
291
+ parsed_headers = None
292
+
293
+ if cmd_args:
294
+ try:
295
+ parsed_args = json.loads(cmd_args)
296
+ except json.JSONDecodeError as e:
297
+ click.echo(f"Error: Invalid JSON for --args: {e}", err=True)
298
+ sys.exit(1)
299
+
300
+ if env:
301
+ try:
302
+ parsed_env = json.loads(env)
303
+ except json.JSONDecodeError as e:
304
+ click.echo(f"Error: Invalid JSON for --env: {e}", err=True)
305
+ sys.exit(1)
306
+
307
+ if headers:
308
+ try:
309
+ parsed_headers = json.loads(headers)
310
+ except json.JSONDecodeError as e:
311
+ click.echo(f"Error: Invalid JSON for --headers: {e}", err=True)
312
+ sys.exit(1)
313
+
314
+ result = call_mcp_api(
315
+ client,
316
+ "/mcp/servers",
317
+ method="POST",
318
+ json_data={
319
+ "name": name,
320
+ "transport": transport,
321
+ "url": url,
322
+ "command": command,
323
+ "args": parsed_args,
324
+ "env": parsed_env,
325
+ "headers": parsed_headers,
326
+ "enabled": not disabled,
327
+ },
328
+ )
329
+ if result is None:
330
+ sys.exit(1)
331
+
332
+ if result.get("success"):
333
+ click.echo(f"Added MCP server: {name}")
334
+ else:
335
+ click.echo(f"Error: {result.get('error', 'Failed to add server')}", err=True)
336
+ sys.exit(1)
337
+
338
+
339
+ @mcp_proxy.command("remove-server")
340
+ @click.argument("name")
341
+ @click.confirmation_option(prompt="Are you sure you want to remove this server?")
342
+ @click.pass_context
343
+ def remove_server(ctx: click.Context, name: str) -> None:
344
+ """Remove an MCP server configuration."""
345
+ client = get_daemon_client(ctx)
346
+ if not check_daemon_running(client):
347
+ sys.exit(1)
348
+
349
+ encoded_name = urllib.parse.quote(name, safe="")
350
+ result = call_mcp_api(
351
+ client,
352
+ f"/mcp/servers/{encoded_name}",
353
+ method="DELETE",
354
+ )
355
+ if result is None:
356
+ sys.exit(1)
357
+
358
+ if result.get("success"):
359
+ click.echo(f"Removed MCP server: {name}")
360
+ else:
361
+ click.echo(f"Error: {result.get('error', 'Failed to remove server')}", err=True)
362
+ sys.exit(1)
363
+
364
+
365
+ @mcp_proxy.command("recommend-tools")
366
+ @click.argument("task_description")
367
+ @click.option("--agent", "-a", "agent_id", help="Agent ID for filtered recommendations")
368
+ @click.option(
369
+ "--mode",
370
+ "-m",
371
+ "search_mode",
372
+ type=click.Choice(["llm", "semantic", "hybrid"]),
373
+ default="llm",
374
+ help="Search mode: llm (default), semantic, or hybrid",
375
+ )
376
+ @click.option("--top-k", "-k", type=int, default=10, help="Max results (semantic/hybrid)")
377
+ @click.option("--json", "json_format", is_flag=True, help="Output as JSON")
378
+ @click.pass_context
379
+ def recommend_tools(
380
+ ctx: click.Context,
381
+ task_description: str,
382
+ agent_id: str | None,
383
+ search_mode: str,
384
+ top_k: int,
385
+ json_format: bool,
386
+ ) -> None:
387
+ """Get AI-powered tool recommendations for a task.
388
+
389
+ Examples:
390
+ gobby mcp-proxy recommend-tools "I need to query a database"
391
+ gobby mcp-proxy recommend-tools "Search for files" --mode semantic
392
+ gobby mcp-proxy recommend-tools "Search for documentation" --agent my-agent
393
+ """
394
+ import os
395
+
396
+ client = get_daemon_client(ctx)
397
+ if not check_daemon_running(client):
398
+ sys.exit(1)
399
+
400
+ result = call_mcp_api(
401
+ client,
402
+ "/mcp/tools/recommend",
403
+ method="POST",
404
+ json_data={
405
+ "task_description": task_description,
406
+ "agent_id": agent_id,
407
+ "search_mode": search_mode,
408
+ "top_k": top_k,
409
+ "cwd": os.getcwd(),
410
+ },
411
+ timeout=120.0, # LLM/embedding generation can be slow
412
+ )
413
+ if result is None:
414
+ sys.exit(1)
415
+
416
+ if json_format:
417
+ click.echo(json.dumps(result, indent=2))
418
+ return
419
+
420
+ recommendations = result.get("recommendations", [])
421
+ if not recommendations:
422
+ click.echo("No tool recommendations found.")
423
+ return
424
+
425
+ click.echo("Recommended tools:")
426
+ for rec in recommendations:
427
+ server = rec.get("server", "unknown")
428
+ tool = rec.get("tool", "unknown")
429
+ reason = rec.get("reason", "")
430
+ click.echo(f" • {server}/{tool}")
431
+ if reason:
432
+ click.echo(f" {reason}")
433
+
434
+
435
+ @mcp_proxy.command("search-tools")
436
+ @click.argument("query")
437
+ @click.option("--top-k", "-k", type=int, default=10, help="Max results to return")
438
+ @click.option("--min-similarity", "-s", type=float, default=0.0, help="Min similarity threshold")
439
+ @click.option("--server", "-S", help="Filter by server name")
440
+ @click.option("--json", "json_format", is_flag=True, help="Output as JSON")
441
+ @click.pass_context
442
+ def search_tools(
443
+ ctx: click.Context,
444
+ query: str,
445
+ top_k: int,
446
+ min_similarity: float,
447
+ server: str | None,
448
+ json_format: bool,
449
+ ) -> None:
450
+ """Search for tools using semantic similarity.
451
+
452
+ Examples:
453
+ gobby mcp-proxy search-tools "query a database"
454
+ gobby mcp-proxy search-tools "search files" --top-k 5
455
+ """
456
+ import os
457
+
458
+ client = get_daemon_client(ctx)
459
+ if not check_daemon_running(client):
460
+ sys.exit(1)
461
+
462
+ result = call_mcp_api(
463
+ client,
464
+ "/mcp/tools/search",
465
+ method="POST",
466
+ json_data={
467
+ "query": query,
468
+ "top_k": top_k,
469
+ "min_similarity": min_similarity,
470
+ "server": server,
471
+ "cwd": os.getcwd(),
472
+ },
473
+ timeout=120.0, # Embedding generation can be slow
474
+ )
475
+ if result is None:
476
+ sys.exit(1)
477
+
478
+ if json_format:
479
+ click.echo(json.dumps(result, indent=2))
480
+ return
481
+
482
+ results = result.get("results", [])
483
+ if not results:
484
+ click.echo("No matching tools found.")
485
+ return
486
+
487
+ click.echo(f"Found {len(results)} tools matching '{query}':")
488
+ for r in results:
489
+ server_name = r.get("server_name", "unknown")
490
+ tool_name = r.get("tool_name", "unknown")
491
+ similarity = r.get("similarity", 0)
492
+ desc = r.get("description", "")
493
+ click.echo(f" • {server_name}/{tool_name} (similarity: {similarity:.2%})")
494
+ if desc:
495
+ # Truncate long descriptions
496
+ if len(desc) > 80:
497
+ desc = desc[:77] + "..."
498
+ click.echo(f" {desc}")
499
+
500
+
501
+ @mcp_proxy.command("import-server")
502
+ @click.option("--from-project", "-p", help="Import from another Gobby project")
503
+ @click.option("--github", "-g", "github_url", help="Import from GitHub repository URL")
504
+ @click.option("--query", "-q", help="Search for MCP server by name/description")
505
+ @click.option("--server", "-s", "servers", multiple=True, help="Specific servers to import")
506
+ @click.option("--json", "json_format", is_flag=True, help="Output as JSON")
507
+ @click.pass_context
508
+ def import_server(
509
+ ctx: click.Context,
510
+ from_project: str | None,
511
+ github_url: str | None,
512
+ query: str | None,
513
+ servers: tuple[str, ...],
514
+ json_format: bool,
515
+ ) -> None:
516
+ """Import MCP server(s) from various sources.
517
+
518
+ Examples:
519
+ gobby mcp-proxy import-server --from-project my-other-project
520
+ gobby mcp-proxy import-server --from-project prod -s context7 -s exa
521
+ gobby mcp-proxy import-server --github https://github.com/user/mcp-server
522
+ gobby mcp-proxy import-server --query "supabase mcp server"
523
+ """
524
+ client = get_daemon_client(ctx)
525
+ if not check_daemon_running(client):
526
+ sys.exit(1)
527
+
528
+ # Validate that at least one source is specified
529
+ if not from_project and not github_url and not query:
530
+ click.echo(
531
+ "Error: Specify at least one source: --from-project, --github, or --query",
532
+ err=True,
533
+ )
534
+ sys.exit(1)
535
+
536
+ result = call_mcp_api(
537
+ client,
538
+ "/mcp/servers/import",
539
+ method="POST",
540
+ json_data={
541
+ "from_project": from_project,
542
+ "github_url": github_url,
543
+ "query": query,
544
+ "servers": list(servers) if servers else None,
545
+ },
546
+ )
547
+ if result is None:
548
+ sys.exit(1)
549
+
550
+ if json_format:
551
+ click.echo(json.dumps(result, indent=2))
552
+ return
553
+
554
+ # Handle different result statuses
555
+ if result.get("status") == "needs_configuration":
556
+ click.echo("Server configuration extracted but needs secrets:")
557
+ config = result.get("config", {})
558
+ click.echo(f" Name: {config.get('name')}")
559
+ click.echo(f" Transport: {config.get('transport')}")
560
+ missing = result.get("missing", [])
561
+ if missing:
562
+ click.echo(f" Missing secrets: {', '.join(missing)}")
563
+ if result.get("instructions"):
564
+ click.echo(f"\nInstructions:\n{result['instructions']}")
565
+ click.echo("\nUse 'gobby mcp-proxy add-server' to add with required values.")
566
+ return
567
+
568
+ if result.get("success"):
569
+ imported = result.get("imported", [])
570
+ if imported:
571
+ click.echo(f"Imported {len(imported)} server(s):")
572
+ for name in imported:
573
+ click.echo(f" + {name}")
574
+
575
+ skipped = result.get("skipped", [])
576
+ if skipped:
577
+ click.echo(f"Skipped {len(skipped)} existing server(s):")
578
+ for name in skipped:
579
+ click.echo(f" - {name}")
580
+
581
+ failed = result.get("failed", [])
582
+ if failed:
583
+ click.echo(f"Failed to import {len(failed)} server(s):")
584
+ for item in failed:
585
+ click.echo(f" x {item.get('name')}: {item.get('error')}")
586
+ else:
587
+ click.echo(f"Error: {result.get('error', 'Import failed')}", err=True)
588
+ if result.get("available_projects"):
589
+ click.echo(f"Available projects: {', '.join(result['available_projects'])}")
590
+ sys.exit(1)
591
+
592
+
593
+ @mcp_proxy.command("refresh")
594
+ @click.option("--force", "-f", is_flag=True, help="Force full refresh, ignore cached hashes")
595
+ @click.option("--server", "-s", help="Only refresh a specific server")
596
+ @click.option("--json", "json_format", is_flag=True, help="Output as JSON")
597
+ @click.pass_context
598
+ def refresh_tools(ctx: click.Context, force: bool, server: str | None, json_format: bool) -> None:
599
+ """Refresh MCP tools - detect schema changes and re-index.
600
+
601
+ Scans all connected MCP servers for tool schema changes and regenerates
602
+ embeddings for new or modified tools. Unchanged tools are skipped.
603
+
604
+ Examples:
605
+ gobby mcp-proxy refresh
606
+ gobby mcp-proxy refresh --force
607
+ gobby mcp-proxy refresh --server context7
608
+ """
609
+ import os
610
+
611
+ client = get_daemon_client(ctx)
612
+ if not check_daemon_running(client):
613
+ sys.exit(1)
614
+
615
+ result = call_mcp_api(
616
+ client,
617
+ "/mcp/refresh",
618
+ method="POST",
619
+ json_data={
620
+ "cwd": os.getcwd(),
621
+ "force": force,
622
+ "server": server,
623
+ },
624
+ timeout=300.0, # Embedding generation can be slow
625
+ )
626
+ if result is None:
627
+ sys.exit(1)
628
+
629
+ if json_format:
630
+ click.echo(json.dumps(result, indent=2))
631
+ return
632
+
633
+ if not result.get("success"):
634
+ click.echo(f"Error: {result.get('error', 'Refresh failed')}", err=True)
635
+ sys.exit(1)
636
+
637
+ stats = result.get("stats", {})
638
+
639
+ # Summary
640
+ click.echo("MCP Tools Refresh Complete")
641
+ click.echo(f" Servers processed: {stats.get('servers_processed', 0)}")
642
+ click.echo(f" New tools: {stats.get('tools_new', 0)}")
643
+ click.echo(f" Changed tools: {stats.get('tools_changed', 0)}")
644
+ click.echo(f" Unchanged tools: {stats.get('tools_unchanged', 0)}")
645
+ click.echo(f" Removed tools: {stats.get('tools_removed', 0)}")
646
+ click.echo(f" Embeddings generated: {stats.get('embeddings_generated', 0)}")
647
+
648
+ if force:
649
+ click.echo("\n(--force: all tools treated as new)")
650
+
651
+ # Per-server breakdown
652
+ by_server = stats.get("by_server", {})
653
+ if by_server:
654
+ click.echo("\nBy Server:")
655
+ for srv_name, srv_stats in by_server.items():
656
+ if "error" in srv_stats:
657
+ click.echo(f" ✗ {srv_name}: {srv_stats['error']}")
658
+ else:
659
+ new = srv_stats.get("new", 0)
660
+ changed = srv_stats.get("changed", 0)
661
+ unchanged = srv_stats.get("unchanged", 0)
662
+ click.echo(f" ● {srv_name}: {new} new, {changed} changed, {unchanged} unchanged")
663
+
664
+
665
+ @mcp_proxy.command("status")
666
+ @click.option("--json", "json_format", is_flag=True, help="Output as JSON")
667
+ @click.pass_context
668
+ def proxy_status(ctx: click.Context, json_format: bool) -> None:
669
+ """Show MCP proxy status and health."""
670
+ client = get_daemon_client(ctx)
671
+ if not check_daemon_running(client):
672
+ sys.exit(1)
673
+
674
+ result = call_mcp_api(client, "/mcp/status", method="GET")
675
+ if result is None:
676
+ sys.exit(1)
677
+
678
+ if json_format:
679
+ click.echo(json.dumps(result, indent=2))
680
+ return
681
+
682
+ click.echo("MCP Proxy Status:")
683
+ click.echo(f" Servers: {result.get('total_servers', 0)}")
684
+ click.echo(f" Connected: {result.get('connected_servers', 0)}")
685
+ click.echo(f" Tools cached: {result.get('cached_tools', 0)}")
686
+
687
+ health = result.get("server_health", {})
688
+ if health:
689
+ click.echo("\nServer Health:")
690
+ for name, info in health.items():
691
+ state = info.get("state", "unknown")
692
+ health_status = info.get("health", "unknown")
693
+ failures = info.get("failures", 0)
694
+ icon = "●" if state == "connected" else "○"
695
+ click.echo(f" {icon} {name}: {state} ({health_status})", nl=False)
696
+ if failures > 0:
697
+ click.echo(f" - {failures} failures", nl=False)
698
+ click.echo()