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,309 @@
1
+ """Tool proxy service."""
2
+
3
+ import logging
4
+ from typing import TYPE_CHECKING, Any
5
+
6
+ from gobby.mcp_proxy.manager import MCPClientManager
7
+ from gobby.mcp_proxy.models import MCPError
8
+
9
+ if TYPE_CHECKING:
10
+ from gobby.mcp_proxy.services.fallback import ToolFallbackResolver
11
+ from gobby.mcp_proxy.services.tool_filter import ToolFilterService
12
+ from gobby.mcp_proxy.tools.internal import InternalRegistryManager
13
+
14
+ logger = logging.getLogger("gobby.mcp.server")
15
+
16
+
17
+ def safe_truncate(text: str | bytes | None, length: int = 100) -> str:
18
+ """Safely truncate text to length by unicode code points."""
19
+ if text is None:
20
+ return ""
21
+ if isinstance(text, bytes):
22
+ text = text.decode("utf-8", errors="replace")
23
+ if len(text) <= length:
24
+ return text
25
+ return text[:length] + "..."
26
+
27
+
28
+ class ToolProxyService:
29
+ """Service for proxying tool calls and resource reads to underlying MCP servers."""
30
+
31
+ def __init__(
32
+ self,
33
+ mcp_manager: MCPClientManager,
34
+ internal_manager: "InternalRegistryManager | None" = None,
35
+ tool_filter: "ToolFilterService | None" = None,
36
+ fallback_resolver: "ToolFallbackResolver | None" = None,
37
+ validate_arguments: bool = True,
38
+ ):
39
+ self._mcp_manager = mcp_manager
40
+ self._internal_manager = internal_manager
41
+ self._tool_filter = tool_filter
42
+ self._fallback_resolver = fallback_resolver
43
+ self._validate_arguments = validate_arguments
44
+
45
+ def _check_arguments(
46
+ self,
47
+ arguments: dict[str, Any],
48
+ schema: dict[str, Any],
49
+ ) -> list[str]:
50
+ """
51
+ Validate arguments against JSON schema.
52
+
53
+ Returns list of validation errors, empty if valid.
54
+ """
55
+ errors = []
56
+ properties = schema.get("properties", {})
57
+ required = schema.get("required", [])
58
+
59
+ # Check for unknown parameters (likely typos like workflow_name vs name)
60
+ for key in arguments:
61
+ if key not in properties:
62
+ # Find similar parameter names for better error message
63
+ similar = [p for p in properties if p in key or key in p]
64
+ if similar:
65
+ errors.append(f"Unknown parameter '{key}'. Did you mean '{similar[0]}'?")
66
+ else:
67
+ valid_params = list(properties.keys())
68
+ errors.append(f"Unknown parameter '{key}'. Valid parameters: {valid_params}")
69
+
70
+ # Check for missing required parameters
71
+ for req in required:
72
+ if req not in arguments:
73
+ errors.append(f"Missing required parameter '{req}'")
74
+
75
+ return errors
76
+
77
+ async def list_tools(
78
+ self,
79
+ server_name: str,
80
+ session_id: str | None = None,
81
+ ) -> dict[str, Any]:
82
+ """
83
+ List tools for a specific server with progressive disclosure format.
84
+
85
+ When session_id is provided and a workflow is active, tools are filtered
86
+ based on the current phase's allowed_tools and blocked_tools settings.
87
+
88
+ Args:
89
+ server_name: Server name (e.g., "gobby-tasks", "context7")
90
+ session_id: Optional session ID to apply workflow phase filtering
91
+
92
+ Returns:
93
+ Dict with tool metadata: {"success": true, "tools": [...], "tool_count": N}
94
+ """
95
+ # Check internal servers first (gobby-tasks, gobby-memory, etc.)
96
+ if self._internal_manager and self._internal_manager.is_internal(server_name):
97
+ registry = self._internal_manager.get_registry(server_name)
98
+ if registry:
99
+ tools = registry.list_tools()
100
+ # Apply phase filtering if session_id provided
101
+ if session_id and self._tool_filter:
102
+ tools = self._tool_filter.filter_tools(tools, session_id)
103
+ return {"success": True, "tools": tools, "tool_count": len(tools)}
104
+ return {
105
+ "success": False,
106
+ "tools": [],
107
+ "error": f"Internal server '{server_name}' not found",
108
+ }
109
+
110
+ # Check external servers
111
+ if self._mcp_manager.has_server(server_name):
112
+ tools_map = await self._mcp_manager.list_tools(server_name)
113
+ tools_list = tools_map.get(server_name, [])
114
+ # Convert to lightweight format
115
+ brief_tools = []
116
+ for tool in tools_list:
117
+ if isinstance(tool, dict):
118
+ brief_tools.append(
119
+ {
120
+ "name": tool.get("name", "unknown"),
121
+ "brief": safe_truncate(tool.get("description", "")),
122
+ }
123
+ )
124
+ else:
125
+ brief_tools.append(
126
+ {
127
+ "name": tool.name,
128
+ "brief": safe_truncate(tool.description),
129
+ }
130
+ )
131
+ # Apply phase filtering if session_id provided
132
+ if session_id and self._tool_filter:
133
+ brief_tools = self._tool_filter.filter_tools(brief_tools, session_id)
134
+ return {"success": True, "tools": brief_tools, "tool_count": len(brief_tools)}
135
+
136
+ return {
137
+ "success": False,
138
+ "tools": [],
139
+ "error": f"Server '{server_name}' not found",
140
+ }
141
+
142
+ async def call_tool(
143
+ self,
144
+ server_name: str,
145
+ tool_name: str,
146
+ arguments: dict[str, Any] | None = None,
147
+ ) -> Any:
148
+ """Execute a tool with optional pre-validation.
149
+
150
+ Pre-validates arguments against the tool's schema before execution.
151
+ On validation error, returns the schema in the error response so
152
+ the caller can self-correct in one round-trip.
153
+
154
+ On execution error, includes fallback_suggestions if a fallback resolver
155
+ is configured.
156
+
157
+ """
158
+ args = arguments or {}
159
+
160
+ # Pre-validate arguments if enabled
161
+ if self._validate_arguments and args:
162
+ schema_result = await self.get_tool_schema(server_name, tool_name)
163
+ if schema_result.get("success"):
164
+ input_schema = schema_result.get("tool", {}).get("inputSchema", {})
165
+ if input_schema:
166
+ validation_errors = self._check_arguments(args, input_schema)
167
+ if validation_errors:
168
+ return {
169
+ "success": False,
170
+ "error": f"Invalid arguments: {validation_errors}",
171
+ "hint": "Review the schema below and retry with correct parameters",
172
+ "schema": input_schema,
173
+ "server_name": server_name,
174
+ "tool_name": tool_name,
175
+ }
176
+
177
+ try:
178
+ # Check internal tools first
179
+ if self._internal_manager and self._internal_manager.is_internal(server_name):
180
+ registry = self._internal_manager.get_registry(server_name)
181
+ if registry:
182
+ return await registry.call(tool_name, args)
183
+ raise MCPError(f"Internal server '{server_name}' not found")
184
+
185
+ # Use MCP manager for external servers
186
+ return await self._mcp_manager.call_tool(server_name, tool_name, arguments)
187
+
188
+ except Exception as e:
189
+ error_message = str(e)
190
+ logger.warning(f"Tool call failed: {server_name}/{tool_name}: {error_message}")
191
+
192
+ # Build error response with fallback suggestions
193
+ response: dict[str, Any] = {
194
+ "success": False,
195
+ "error": error_message,
196
+ "server_name": server_name,
197
+ "tool_name": tool_name,
198
+ }
199
+
200
+ # Get fallback suggestions if resolver is available
201
+ if self._fallback_resolver:
202
+ try:
203
+ project_id = self._mcp_manager.project_id
204
+ if project_id:
205
+ suggestions = await self._fallback_resolver.find_alternatives_for_error(
206
+ server_name=server_name,
207
+ tool_name=tool_name,
208
+ error_message=error_message,
209
+ project_id=project_id,
210
+ )
211
+ response["fallback_suggestions"] = suggestions
212
+ else:
213
+ response["fallback_suggestions"] = []
214
+ except Exception as fallback_error:
215
+ logger.debug(f"Fallback resolver failed: {fallback_error}")
216
+ response["fallback_suggestions"] = []
217
+ else:
218
+ response["fallback_suggestions"] = []
219
+
220
+ return response
221
+
222
+ async def read_resource(self, server_name: str, uri: str) -> Any:
223
+ """Read a resource."""
224
+ return await self._mcp_manager.read_resource(server_name, uri)
225
+
226
+ async def get_tool_schema(self, server_name: str, tool_name: str) -> dict[str, Any]:
227
+ """Get full schema for a specific tool."""
228
+ # Check internal tools first
229
+ if self._internal_manager and self._internal_manager.is_internal(server_name):
230
+ registry = self._internal_manager.get_registry(server_name)
231
+ if registry:
232
+ schema = registry.get_schema(tool_name)
233
+ if schema:
234
+ return {"success": True, "tool": schema}
235
+ return {
236
+ "success": False,
237
+ "error": f"Tool '{tool_name}' not found on '{server_name}'",
238
+ }
239
+ return {"success": False, "error": f"Internal server '{server_name}' not found"}
240
+
241
+ if not self._mcp_manager.has_server(server_name):
242
+ return {"success": False, "error": f"Server '{server_name}' not found"}
243
+
244
+ # Use MCP manager for external servers
245
+ try:
246
+ return await self._mcp_manager.get_tool_input_schema(server_name, tool_name)
247
+ except Exception as e:
248
+ raise MCPError(f"Failed to get schema for {tool_name} on {server_name}: {e}") from e
249
+
250
+ def find_tool_server(self, tool_name: str) -> str | None:
251
+ """
252
+ Find which server owns a tool by searching all available servers.
253
+
254
+ Searches internal registries first (faster), then external server configs.
255
+
256
+ Args:
257
+ tool_name: Name of the tool to find
258
+
259
+ Returns:
260
+ Server name if found, None otherwise
261
+ """
262
+ # Search internal registries first (fast, in-memory lookup)
263
+ if self._internal_manager:
264
+ server = self._internal_manager.find_tool_server(tool_name)
265
+ if server:
266
+ return server
267
+
268
+ # Search external server configs (cached tool metadata)
269
+ for server_name, config in self._mcp_manager._configs.items():
270
+ if config.tools:
271
+ for tool in config.tools:
272
+ tool_name_in_config = (
273
+ tool.get("name") if isinstance(tool, dict) else getattr(tool, "name", None)
274
+ )
275
+ if tool_name_in_config == tool_name:
276
+ return server_name
277
+
278
+ return None
279
+
280
+ async def call_tool_by_name(
281
+ self,
282
+ tool_name: str,
283
+ arguments: dict[str, Any] | None = None,
284
+ ) -> Any:
285
+ """
286
+ Call a tool by name, automatically resolving the server.
287
+
288
+ Searches all available servers to find which one owns the tool,
289
+ then routes the call appropriately.
290
+
291
+ Args:
292
+ tool_name: Name of the tool to call
293
+ arguments: Tool arguments
294
+
295
+ Returns:
296
+ Tool execution result, or error dict if tool not found
297
+ """
298
+ server_name = self.find_tool_server(tool_name)
299
+
300
+ if server_name is None:
301
+ logger.warning(f"Tool '{tool_name}' not found on any server")
302
+ return {
303
+ "success": False,
304
+ "error": f"Tool '{tool_name}' not found on any available server",
305
+ "tool_name": tool_name,
306
+ }
307
+
308
+ logger.debug(f"Routing tool '{tool_name}' to server '{server_name}'")
309
+ return await self.call_tool(server_name, tool_name, arguments)