gobby 0.2.5__py3-none-any.whl → 0.2.7__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 (244) hide show
  1. gobby/__init__.py +1 -1
  2. gobby/adapters/__init__.py +2 -1
  3. gobby/adapters/claude_code.py +13 -4
  4. gobby/adapters/codex_impl/__init__.py +28 -0
  5. gobby/adapters/codex_impl/adapter.py +722 -0
  6. gobby/adapters/codex_impl/client.py +679 -0
  7. gobby/adapters/codex_impl/protocol.py +20 -0
  8. gobby/adapters/codex_impl/types.py +68 -0
  9. gobby/agents/definitions.py +11 -1
  10. gobby/agents/isolation.py +395 -0
  11. gobby/agents/runner.py +8 -0
  12. gobby/agents/sandbox.py +261 -0
  13. gobby/agents/spawn.py +42 -287
  14. gobby/agents/spawn_executor.py +385 -0
  15. gobby/agents/spawners/__init__.py +24 -0
  16. gobby/agents/spawners/command_builder.py +189 -0
  17. gobby/agents/spawners/embedded.py +21 -2
  18. gobby/agents/spawners/headless.py +21 -2
  19. gobby/agents/spawners/prompt_manager.py +125 -0
  20. gobby/cli/__init__.py +6 -0
  21. gobby/cli/clones.py +419 -0
  22. gobby/cli/conductor.py +266 -0
  23. gobby/cli/install.py +4 -4
  24. gobby/cli/installers/antigravity.py +3 -9
  25. gobby/cli/installers/claude.py +15 -9
  26. gobby/cli/installers/codex.py +2 -8
  27. gobby/cli/installers/gemini.py +8 -8
  28. gobby/cli/installers/shared.py +175 -13
  29. gobby/cli/sessions.py +1 -1
  30. gobby/cli/skills.py +858 -0
  31. gobby/cli/tasks/ai.py +0 -440
  32. gobby/cli/tasks/crud.py +44 -6
  33. gobby/cli/tasks/main.py +0 -4
  34. gobby/cli/tui.py +2 -2
  35. gobby/cli/utils.py +12 -5
  36. gobby/clones/__init__.py +13 -0
  37. gobby/clones/git.py +547 -0
  38. gobby/conductor/__init__.py +16 -0
  39. gobby/conductor/alerts.py +135 -0
  40. gobby/conductor/loop.py +164 -0
  41. gobby/conductor/monitors/__init__.py +11 -0
  42. gobby/conductor/monitors/agents.py +116 -0
  43. gobby/conductor/monitors/tasks.py +155 -0
  44. gobby/conductor/pricing.py +234 -0
  45. gobby/conductor/token_tracker.py +160 -0
  46. gobby/config/__init__.py +12 -97
  47. gobby/config/app.py +69 -91
  48. gobby/config/extensions.py +2 -2
  49. gobby/config/features.py +7 -130
  50. gobby/config/search.py +110 -0
  51. gobby/config/servers.py +1 -1
  52. gobby/config/skills.py +43 -0
  53. gobby/config/tasks.py +9 -41
  54. gobby/hooks/__init__.py +0 -13
  55. gobby/hooks/event_handlers.py +188 -2
  56. gobby/hooks/hook_manager.py +50 -4
  57. gobby/hooks/plugins.py +1 -1
  58. gobby/hooks/skill_manager.py +130 -0
  59. gobby/hooks/webhooks.py +1 -1
  60. gobby/install/claude/hooks/hook_dispatcher.py +4 -4
  61. gobby/install/codex/hooks/hook_dispatcher.py +1 -1
  62. gobby/install/gemini/hooks/hook_dispatcher.py +87 -12
  63. gobby/llm/claude.py +22 -34
  64. gobby/llm/claude_executor.py +46 -256
  65. gobby/llm/codex_executor.py +59 -291
  66. gobby/llm/executor.py +21 -0
  67. gobby/llm/gemini.py +134 -110
  68. gobby/llm/litellm_executor.py +143 -6
  69. gobby/llm/resolver.py +98 -35
  70. gobby/mcp_proxy/importer.py +62 -4
  71. gobby/mcp_proxy/instructions.py +56 -0
  72. gobby/mcp_proxy/models.py +15 -0
  73. gobby/mcp_proxy/registries.py +68 -8
  74. gobby/mcp_proxy/server.py +33 -3
  75. gobby/mcp_proxy/services/recommendation.py +43 -11
  76. gobby/mcp_proxy/services/tool_proxy.py +81 -1
  77. gobby/mcp_proxy/stdio.py +2 -1
  78. gobby/mcp_proxy/tools/__init__.py +0 -2
  79. gobby/mcp_proxy/tools/agent_messaging.py +317 -0
  80. gobby/mcp_proxy/tools/agents.py +31 -731
  81. gobby/mcp_proxy/tools/clones.py +518 -0
  82. gobby/mcp_proxy/tools/memory.py +3 -26
  83. gobby/mcp_proxy/tools/metrics.py +65 -1
  84. gobby/mcp_proxy/tools/orchestration/__init__.py +3 -0
  85. gobby/mcp_proxy/tools/orchestration/cleanup.py +151 -0
  86. gobby/mcp_proxy/tools/orchestration/wait.py +467 -0
  87. gobby/mcp_proxy/tools/sessions/__init__.py +14 -0
  88. gobby/mcp_proxy/tools/sessions/_commits.py +232 -0
  89. gobby/mcp_proxy/tools/sessions/_crud.py +253 -0
  90. gobby/mcp_proxy/tools/sessions/_factory.py +63 -0
  91. gobby/mcp_proxy/tools/sessions/_handoff.py +499 -0
  92. gobby/mcp_proxy/tools/sessions/_messages.py +138 -0
  93. gobby/mcp_proxy/tools/skills/__init__.py +616 -0
  94. gobby/mcp_proxy/tools/spawn_agent.py +417 -0
  95. gobby/mcp_proxy/tools/task_orchestration.py +7 -0
  96. gobby/mcp_proxy/tools/task_readiness.py +14 -0
  97. gobby/mcp_proxy/tools/task_sync.py +1 -1
  98. gobby/mcp_proxy/tools/tasks/_context.py +0 -20
  99. gobby/mcp_proxy/tools/tasks/_crud.py +91 -4
  100. gobby/mcp_proxy/tools/tasks/_expansion.py +348 -0
  101. gobby/mcp_proxy/tools/tasks/_factory.py +6 -16
  102. gobby/mcp_proxy/tools/tasks/_lifecycle.py +110 -45
  103. gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +18 -29
  104. gobby/mcp_proxy/tools/workflows.py +1 -1
  105. gobby/mcp_proxy/tools/worktrees.py +0 -338
  106. gobby/memory/backends/__init__.py +6 -1
  107. gobby/memory/backends/mem0.py +6 -1
  108. gobby/memory/extractor.py +477 -0
  109. gobby/memory/ingestion/__init__.py +5 -0
  110. gobby/memory/ingestion/multimodal.py +221 -0
  111. gobby/memory/manager.py +73 -285
  112. gobby/memory/search/__init__.py +10 -0
  113. gobby/memory/search/coordinator.py +248 -0
  114. gobby/memory/services/__init__.py +5 -0
  115. gobby/memory/services/crossref.py +142 -0
  116. gobby/prompts/loader.py +5 -2
  117. gobby/runner.py +37 -16
  118. gobby/search/__init__.py +48 -6
  119. gobby/search/backends/__init__.py +159 -0
  120. gobby/search/backends/embedding.py +225 -0
  121. gobby/search/embeddings.py +238 -0
  122. gobby/search/models.py +148 -0
  123. gobby/search/unified.py +496 -0
  124. gobby/servers/http.py +24 -12
  125. gobby/servers/routes/admin.py +294 -0
  126. gobby/servers/routes/mcp/endpoints/__init__.py +61 -0
  127. gobby/servers/routes/mcp/endpoints/discovery.py +405 -0
  128. gobby/servers/routes/mcp/endpoints/execution.py +568 -0
  129. gobby/servers/routes/mcp/endpoints/registry.py +378 -0
  130. gobby/servers/routes/mcp/endpoints/server.py +304 -0
  131. gobby/servers/routes/mcp/hooks.py +1 -1
  132. gobby/servers/routes/mcp/tools.py +48 -1317
  133. gobby/servers/websocket.py +2 -2
  134. gobby/sessions/analyzer.py +2 -0
  135. gobby/sessions/lifecycle.py +1 -1
  136. gobby/sessions/processor.py +10 -0
  137. gobby/sessions/transcripts/base.py +2 -0
  138. gobby/sessions/transcripts/claude.py +79 -10
  139. gobby/skills/__init__.py +91 -0
  140. gobby/skills/loader.py +685 -0
  141. gobby/skills/manager.py +384 -0
  142. gobby/skills/parser.py +286 -0
  143. gobby/skills/search.py +463 -0
  144. gobby/skills/sync.py +119 -0
  145. gobby/skills/updater.py +385 -0
  146. gobby/skills/validator.py +368 -0
  147. gobby/storage/clones.py +378 -0
  148. gobby/storage/database.py +1 -1
  149. gobby/storage/memories.py +43 -13
  150. gobby/storage/migrations.py +162 -201
  151. gobby/storage/sessions.py +116 -7
  152. gobby/storage/skills.py +782 -0
  153. gobby/storage/tasks/_crud.py +4 -4
  154. gobby/storage/tasks/_lifecycle.py +57 -7
  155. gobby/storage/tasks/_manager.py +14 -5
  156. gobby/storage/tasks/_models.py +8 -3
  157. gobby/sync/memories.py +40 -5
  158. gobby/sync/tasks.py +83 -6
  159. gobby/tasks/__init__.py +1 -2
  160. gobby/tasks/external_validator.py +1 -1
  161. gobby/tasks/validation.py +46 -35
  162. gobby/tools/summarizer.py +91 -10
  163. gobby/tui/api_client.py +4 -7
  164. gobby/tui/app.py +5 -3
  165. gobby/tui/screens/orchestrator.py +1 -2
  166. gobby/tui/screens/tasks.py +2 -4
  167. gobby/tui/ws_client.py +1 -1
  168. gobby/utils/daemon_client.py +2 -2
  169. gobby/utils/project_context.py +2 -3
  170. gobby/utils/status.py +13 -0
  171. gobby/workflows/actions.py +221 -1135
  172. gobby/workflows/artifact_actions.py +31 -0
  173. gobby/workflows/autonomous_actions.py +11 -0
  174. gobby/workflows/context_actions.py +93 -1
  175. gobby/workflows/detection_helpers.py +115 -31
  176. gobby/workflows/enforcement/__init__.py +47 -0
  177. gobby/workflows/enforcement/blocking.py +269 -0
  178. gobby/workflows/enforcement/commit_policy.py +283 -0
  179. gobby/workflows/enforcement/handlers.py +269 -0
  180. gobby/workflows/{task_enforcement_actions.py → enforcement/task_policy.py} +29 -388
  181. gobby/workflows/engine.py +13 -2
  182. gobby/workflows/git_utils.py +106 -0
  183. gobby/workflows/lifecycle_evaluator.py +29 -1
  184. gobby/workflows/llm_actions.py +30 -0
  185. gobby/workflows/loader.py +19 -6
  186. gobby/workflows/mcp_actions.py +20 -1
  187. gobby/workflows/memory_actions.py +154 -0
  188. gobby/workflows/safe_evaluator.py +183 -0
  189. gobby/workflows/session_actions.py +44 -0
  190. gobby/workflows/state_actions.py +60 -1
  191. gobby/workflows/stop_signal_actions.py +55 -0
  192. gobby/workflows/summary_actions.py +111 -1
  193. gobby/workflows/task_sync_actions.py +347 -0
  194. gobby/workflows/todo_actions.py +34 -1
  195. gobby/workflows/webhook_actions.py +185 -0
  196. {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/METADATA +87 -21
  197. {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/RECORD +201 -172
  198. {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/WHEEL +1 -1
  199. gobby/adapters/codex.py +0 -1292
  200. gobby/install/claude/commands/gobby/bug.md +0 -51
  201. gobby/install/claude/commands/gobby/chore.md +0 -51
  202. gobby/install/claude/commands/gobby/epic.md +0 -52
  203. gobby/install/claude/commands/gobby/eval.md +0 -235
  204. gobby/install/claude/commands/gobby/feat.md +0 -49
  205. gobby/install/claude/commands/gobby/nit.md +0 -52
  206. gobby/install/claude/commands/gobby/ref.md +0 -52
  207. gobby/install/codex/prompts/forget.md +0 -7
  208. gobby/install/codex/prompts/memories.md +0 -7
  209. gobby/install/codex/prompts/recall.md +0 -7
  210. gobby/install/codex/prompts/remember.md +0 -13
  211. gobby/llm/gemini_executor.py +0 -339
  212. gobby/mcp_proxy/tools/session_messages.py +0 -1056
  213. gobby/mcp_proxy/tools/task_expansion.py +0 -591
  214. gobby/prompts/defaults/expansion/system.md +0 -119
  215. gobby/prompts/defaults/expansion/user.md +0 -48
  216. gobby/prompts/defaults/external_validation/agent.md +0 -72
  217. gobby/prompts/defaults/external_validation/external.md +0 -63
  218. gobby/prompts/defaults/external_validation/spawn.md +0 -83
  219. gobby/prompts/defaults/external_validation/system.md +0 -6
  220. gobby/prompts/defaults/features/import_mcp.md +0 -22
  221. gobby/prompts/defaults/features/import_mcp_github.md +0 -17
  222. gobby/prompts/defaults/features/import_mcp_search.md +0 -16
  223. gobby/prompts/defaults/features/recommend_tools.md +0 -32
  224. gobby/prompts/defaults/features/recommend_tools_hybrid.md +0 -35
  225. gobby/prompts/defaults/features/recommend_tools_llm.md +0 -30
  226. gobby/prompts/defaults/features/server_description.md +0 -20
  227. gobby/prompts/defaults/features/server_description_system.md +0 -6
  228. gobby/prompts/defaults/features/task_description.md +0 -31
  229. gobby/prompts/defaults/features/task_description_system.md +0 -6
  230. gobby/prompts/defaults/features/tool_summary.md +0 -17
  231. gobby/prompts/defaults/features/tool_summary_system.md +0 -6
  232. gobby/prompts/defaults/research/step.md +0 -58
  233. gobby/prompts/defaults/validation/criteria.md +0 -47
  234. gobby/prompts/defaults/validation/validate.md +0 -38
  235. gobby/storage/migrations_legacy.py +0 -1359
  236. gobby/tasks/context.py +0 -747
  237. gobby/tasks/criteria.py +0 -342
  238. gobby/tasks/expansion.py +0 -626
  239. gobby/tasks/prompts/expand.py +0 -327
  240. gobby/tasks/research.py +0 -421
  241. gobby/tasks/tdd.py +0 -352
  242. {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/entry_points.txt +0 -0
  243. {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/licenses/LICENSE.md +0 -0
  244. {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/top_level.txt +0 -0
@@ -1,32 +1,37 @@
1
1
  """
2
2
  MCP routes for Gobby HTTP server.
3
3
 
4
- Provides MCP server management, tool discovery, and tool execution endpoints.
5
- Uses FastAPI dependency injection via Depends() for proper testability.
4
+ Thin router aggregation layer that composes endpoints from:
5
+ - endpoints/discovery.py - Tool listing and search
6
+ - endpoints/execution.py - Tool calls and schema retrieval
7
+ - endpoints/server.py - Server management
8
+ - endpoints/registry.py - Embedding, status, and refresh
6
9
  """
7
10
 
8
- import json
9
- import logging
10
- import time
11
- from typing import TYPE_CHECKING, Any
11
+ from fastapi import APIRouter
12
12
 
13
- from fastapi import APIRouter, Depends, HTTPException, Request
14
-
15
- from gobby.servers.routes.dependencies import (
16
- get_internal_manager,
17
- get_mcp_manager,
18
- get_metrics_manager,
19
- get_server,
13
+ from gobby.servers.routes.mcp.endpoints.discovery import (
14
+ list_all_mcp_tools,
15
+ recommend_mcp_tools,
16
+ search_mcp_tools,
17
+ )
18
+ from gobby.servers.routes.mcp.endpoints.execution import (
19
+ call_mcp_tool,
20
+ get_tool_schema,
21
+ list_mcp_tools,
22
+ mcp_proxy,
23
+ )
24
+ from gobby.servers.routes.mcp.endpoints.registry import (
25
+ embed_mcp_tools,
26
+ get_mcp_status,
27
+ refresh_mcp_tools,
28
+ )
29
+ from gobby.servers.routes.mcp.endpoints.server import (
30
+ add_mcp_server,
31
+ import_mcp_server,
32
+ list_mcp_servers,
33
+ remove_mcp_server,
20
34
  )
21
- from gobby.utils.metrics import get_metrics_collector
22
-
23
- if TYPE_CHECKING:
24
- from gobby.mcp_proxy.manager import MCPClientManager
25
- from gobby.mcp_proxy.metrics import ToolMetricsManager
26
- from gobby.mcp_proxy.registry_manager import InternalToolRegistryManager
27
- from gobby.servers.http import HTTPServer
28
-
29
- logger = logging.getLogger(__name__)
30
35
 
31
36
 
32
37
  def create_mcp_router() -> APIRouter:
@@ -37,1301 +42,27 @@ def create_mcp_router() -> APIRouter:
37
42
  Configured APIRouter with MCP endpoints
38
43
  """
39
44
  router = APIRouter(prefix="/mcp", tags=["mcp"])
40
- metrics = get_metrics_collector()
41
-
42
- @router.get("/{server_name}/tools")
43
- async def list_mcp_tools(
44
- server_name: str,
45
- internal_manager: "InternalToolRegistryManager | None" = Depends(get_internal_manager),
46
- mcp_manager: "MCPClientManager | None" = Depends(get_mcp_manager),
47
- ) -> dict[str, Any]:
48
- """
49
- List available tools from an MCP server.
50
-
51
- Args:
52
- server_name: Name of the MCP server (e.g., "supabase", "context7")
53
- internal_manager: Internal tool registry manager (injected)
54
- mcp_manager: External MCP client manager (injected)
55
-
56
- Returns:
57
- List of available tools with their descriptions
58
- """
59
- start_time = time.perf_counter()
60
- metrics.inc_counter("http_requests_total")
61
-
62
- try:
63
- # Check internal registries first (gobby-tasks, gobby-memory, etc.)
64
- if internal_manager and internal_manager.is_internal(server_name):
65
- registry = internal_manager.get_registry(server_name)
66
- if registry:
67
- tools = registry.list_tools()
68
- response_time_ms = (time.perf_counter() - start_time) * 1000
69
- metrics.observe_histogram("list_mcp_tools", response_time_ms / 1000)
70
- return {
71
- "status": "success",
72
- "tools": tools,
73
- "tool_count": len(tools),
74
- "response_time_ms": response_time_ms,
75
- }
76
- raise HTTPException(
77
- status_code=404, detail=f"Internal server '{server_name}' not found"
78
- )
79
-
80
- if mcp_manager is None:
81
- raise HTTPException(status_code=503, detail="MCP manager not available")
82
-
83
- # Check if server is configured
84
- if not mcp_manager.has_server(server_name):
85
- raise HTTPException(status_code=404, detail=f"Unknown MCP server: '{server_name}'")
86
-
87
- # Use ensure_connected for lazy loading - connects on-demand if not connected
88
- try:
89
- session = await mcp_manager.ensure_connected(server_name)
90
- except KeyError as e:
91
- raise HTTPException(status_code=404, detail=str(e)) from e
92
- except Exception as e:
93
- raise HTTPException(
94
- status_code=503,
95
- detail=f"MCP server '{server_name}' connection failed: {e}",
96
- ) from e
97
-
98
- # List tools using MCP SDK
99
- try:
100
- tools_result = await session.list_tools()
101
- tools = []
102
- for tool in tools_result.tools:
103
- tool_dict: dict[str, Any] = {
104
- "name": tool.name,
105
- "description": tool.description if hasattr(tool, "description") else None,
106
- }
107
-
108
- # Handle inputSchema
109
- if hasattr(tool, "inputSchema"):
110
- schema = tool.inputSchema
111
- if hasattr(schema, "model_dump"):
112
- tool_dict["inputSchema"] = schema.model_dump()
113
- elif isinstance(schema, dict):
114
- tool_dict["inputSchema"] = schema
115
- else:
116
- tool_dict["inputSchema"] = None
117
- else:
118
- tool_dict["inputSchema"] = None
119
-
120
- tools.append(tool_dict)
121
-
122
- response_time_ms = (time.perf_counter() - start_time) * 1000
123
-
124
- logger.debug(
125
- f"Listed {len(tools)} tools from {server_name}",
126
- extra={
127
- "server": server_name,
128
- "tool_count": len(tools),
129
- "response_time_ms": response_time_ms,
130
- },
131
- )
132
-
133
- return {
134
- "status": "success",
135
- "tools": tools,
136
- "tool_count": len(tools),
137
- "response_time_ms": response_time_ms,
138
- }
139
-
140
- except Exception as e:
141
- logger.error(
142
- f"Failed to list tools from {server_name}: {e}",
143
- exc_info=True,
144
- extra={"server": server_name},
145
- )
146
- raise HTTPException(status_code=500, detail=f"Failed to list tools: {e}") from e
147
-
148
- except HTTPException:
149
- raise
150
- except Exception as e:
151
- metrics.inc_counter("http_requests_errors_total")
152
- logger.error(f"MCP list tools error: {server_name}", exc_info=True)
153
- raise HTTPException(status_code=500, detail=str(e)) from e
154
-
155
- @router.get("/servers")
156
- async def list_mcp_servers(
157
- internal_manager: "InternalToolRegistryManager | None" = Depends(get_internal_manager),
158
- mcp_manager: "MCPClientManager | None" = Depends(get_mcp_manager),
159
- ) -> dict[str, Any]:
160
- """
161
- List all configured MCP servers.
162
-
163
- Args:
164
- internal_manager: Internal tool registry manager (injected)
165
- mcp_manager: External MCP client manager (injected)
166
-
167
- Returns:
168
- List of servers with connection status
169
- """
170
- start_time = time.perf_counter()
171
- metrics.inc_counter("http_requests_total")
172
-
173
- try:
174
- server_list = []
175
-
176
- # Add internal servers (gobby-tasks, gobby-memory, etc.)
177
- if internal_manager:
178
- for registry in internal_manager.get_all_registries():
179
- server_list.append(
180
- {
181
- "name": registry.name,
182
- "state": "connected",
183
- "connected": True,
184
- "transport": "internal",
185
- }
186
- )
187
-
188
- # Add external MCP servers
189
- if mcp_manager:
190
- for config in mcp_manager.server_configs:
191
- health = mcp_manager.health.get(config.name)
192
- is_connected = config.name in mcp_manager.connections
193
- server_list.append(
194
- {
195
- "name": config.name,
196
- "state": health.state.value if health else "unknown",
197
- "connected": is_connected,
198
- "transport": config.transport,
199
- "enabled": config.enabled,
200
- }
201
- )
202
-
203
- response_time_ms = (time.perf_counter() - start_time) * 1000
204
-
205
- return {
206
- "servers": server_list,
207
- "total_count": len(server_list),
208
- "connected_count": len([s for s in server_list if s.get("connected")]),
209
- "response_time_ms": response_time_ms,
210
- }
211
-
212
- except Exception as e:
213
- metrics.inc_counter("http_requests_errors_total")
214
- logger.error(f"List MCP servers error: {e}", exc_info=True)
215
- raise HTTPException(status_code=500, detail=str(e)) from e
216
-
217
- @router.get("/tools")
218
- async def list_all_mcp_tools(
219
- server_filter: str | None = None,
220
- include_metrics: bool = False,
221
- project_id: str | None = None,
222
- server: "HTTPServer" = Depends(get_server),
223
- metrics_manager: "ToolMetricsManager | None" = Depends(get_metrics_manager),
224
- ) -> dict[str, Any]:
225
- """
226
- List tools from MCP servers.
227
-
228
- Args:
229
- server_filter: Optional server name to filter by
230
- include_metrics: When True, include call_count, success_rate, avg_latency for each tool
231
- project_id: Project ID for metrics lookup (uses current project if not specified)
232
-
233
- Returns:
234
- Dict of server names to tool lists
235
- """
236
- start_time = time.perf_counter()
237
- metrics.inc_counter("http_requests_total")
238
-
239
- try:
240
- tools_by_server: dict[str, list[dict[str, Any]]] = {}
241
-
242
- # Resolve project_id for metrics lookup
243
- resolved_project_id = None
244
- if include_metrics:
245
- try:
246
- resolved_project_id = server._resolve_project_id(project_id, cwd=None)
247
- except ValueError:
248
- # Project not initialized; skip metrics enrichment
249
- resolved_project_id = None
250
-
251
- # If specific server requested
252
- if server_filter:
253
- # Check internal first
254
- if server._internal_manager and server._internal_manager.is_internal(server_filter):
255
- registry = server._internal_manager.get_registry(server_filter)
256
- if registry:
257
- tools_by_server[server_filter] = registry.list_tools()
258
- elif server.mcp_manager and server.mcp_manager.has_server(server_filter):
259
- # Check if server is enabled before attempting connection
260
- server_config = server.mcp_manager._configs.get(server_filter)
261
- if server_config and not server_config.enabled:
262
- tools_by_server[server_filter] = []
263
- else:
264
- try:
265
- # Use ensure_connected for lazy loading
266
- session = await server.mcp_manager.ensure_connected(server_filter)
267
- tools_result = await session.list_tools()
268
- tools_list = []
269
- for t in tools_result.tools:
270
- desc = getattr(t, "description", "") or ""
271
- tools_list.append(
272
- {
273
- "name": t.name,
274
- "brief": desc[:100],
275
- }
276
- )
277
- tools_by_server[server_filter] = tools_list
278
- except Exception as e:
279
- logger.warning(f"Failed to list tools from {server_filter}: {e}")
280
- tools_by_server[server_filter] = []
281
- else:
282
- # Get tools from all servers
283
- # Internal servers
284
- if server._internal_manager:
285
- for registry in server._internal_manager.get_all_registries():
286
- tools_by_server[registry.name] = registry.list_tools()
287
-
288
- # External MCP servers - use ensure_connected for lazy loading
289
- if server.mcp_manager:
290
- for config in server.mcp_manager.server_configs:
291
- if config.enabled:
292
- try:
293
- session = await server.mcp_manager.ensure_connected(config.name)
294
- tools_result = await session.list_tools()
295
- tools_list = []
296
- for t in tools_result.tools:
297
- desc = getattr(t, "description", "") or ""
298
- tools_list.append(
299
- {
300
- "name": t.name,
301
- "brief": desc[:100],
302
- }
303
- )
304
- tools_by_server[config.name] = tools_list
305
- except Exception as e:
306
- logger.warning(f"Failed to list tools from {config.name}: {e}")
307
- tools_by_server[config.name] = []
308
-
309
- # Enrich with metrics if requested
310
- if include_metrics and metrics_manager and resolved_project_id:
311
- # Get all metrics for this project
312
- metrics_data = metrics_manager.get_metrics(project_id=resolved_project_id)
313
- metrics_by_key = {
314
- (m["server_name"], m["tool_name"]): m for m in metrics_data.get("tools", [])
315
- }
316
-
317
- for server_name, tools_list in tools_by_server.items():
318
- for tool in tools_list:
319
- # Guard against non-dict or missing-name entries
320
- if not isinstance(tool, dict) or "name" not in tool:
321
- continue
322
- tool_name = tool.get("name")
323
- key = (server_name, tool_name)
324
- if key in metrics_by_key:
325
- m = metrics_by_key[key]
326
- tool["call_count"] = m.get("call_count", 0)
327
- tool["success_rate"] = m.get("success_rate")
328
- tool["avg_latency_ms"] = m.get("avg_latency_ms")
329
- else:
330
- tool["call_count"] = 0
331
- tool["success_rate"] = None
332
- tool["avg_latency_ms"] = None
333
-
334
- response_time_ms = (time.perf_counter() - start_time) * 1000
335
-
336
- return {
337
- "tools": tools_by_server,
338
- "response_time_ms": response_time_ms,
339
- }
340
-
341
- except Exception as e:
342
- metrics.inc_counter("http_requests_errors_total")
343
- logger.error(f"List MCP tools error: {e}", exc_info=True)
344
- raise HTTPException(status_code=500, detail=str(e)) from e
345
-
346
- @router.post("/tools/schema")
347
- async def get_tool_schema(
348
- request: Request,
349
- server: "HTTPServer" = Depends(get_server),
350
- ) -> dict[str, Any]:
351
- """
352
- Get full schema for a specific tool.
353
-
354
- Request body:
355
- {
356
- "server_name": "supabase",
357
- "tool_name": "list_tables"
358
- }
359
-
360
- Returns:
361
- Tool schema with inputSchema
362
- """
363
- start_time = time.perf_counter()
364
- metrics.inc_counter("http_requests_total")
365
-
366
- try:
367
- body = await request.json()
368
- server_name = body.get("server_name")
369
- tool_name = body.get("tool_name")
370
-
371
- if not server_name or not tool_name:
372
- raise HTTPException(
373
- status_code=400, detail="Required fields: server_name, tool_name"
374
- )
375
-
376
- # Check internal first
377
- if server._internal_manager and server._internal_manager.is_internal(server_name):
378
- registry = server._internal_manager.get_registry(server_name)
379
- if registry:
380
- schema = registry.get_schema(tool_name)
381
- if schema:
382
- response_time_ms = (time.perf_counter() - start_time) * 1000
383
- return {
384
- "name": tool_name,
385
- "server": server_name,
386
- "inputSchema": schema,
387
- "response_time_ms": response_time_ms,
388
- }
389
- raise HTTPException(
390
- status_code=404,
391
- detail=f"Tool '{tool_name}' not found on server '{server_name}'",
392
- )
393
-
394
- if server.mcp_manager is None:
395
- raise HTTPException(status_code=503, detail="MCP manager not available")
396
-
397
- # Get from external MCP server
398
- try:
399
- schema = await server.mcp_manager.get_tool_input_schema(server_name, tool_name)
400
- response_time_ms = (time.perf_counter() - start_time) * 1000
401
-
402
- return {
403
- "name": tool_name,
404
- "server": server_name,
405
- "inputSchema": schema,
406
- "response_time_ms": response_time_ms,
407
- }
408
-
409
- except Exception as e:
410
- raise HTTPException(status_code=404, detail=str(e)) from e
411
-
412
- except HTTPException:
413
- raise
414
- except Exception as e:
415
- metrics.inc_counter("http_requests_errors_total")
416
- logger.error(f"Get tool schema error: {e}", exc_info=True)
417
- raise HTTPException(status_code=500, detail=str(e)) from e
418
-
419
- @router.post("/tools/call")
420
- async def call_mcp_tool(
421
- request: Request,
422
- server: "HTTPServer" = Depends(get_server),
423
- ) -> dict[str, Any]:
424
- """
425
- Call an MCP tool.
426
-
427
- Request body:
428
- {
429
- "server_name": "supabase",
430
- "tool_name": "list_tables",
431
- "arguments": {}
432
- }
433
-
434
- Returns:
435
- Tool execution result
436
- """
437
- start_time = time.perf_counter()
438
- metrics.inc_counter("http_requests_total")
439
- metrics.inc_counter("mcp_tool_calls_total")
440
-
441
- try:
442
- body = await request.json()
443
- server_name = body.get("server_name")
444
- tool_name = body.get("tool_name")
445
- arguments = body.get("arguments", {})
446
-
447
- if not server_name or not tool_name:
448
- raise HTTPException(
449
- status_code=400, detail="Required fields: server_name, tool_name"
450
- )
451
-
452
- # Check internal first
453
- if server._internal_manager and server._internal_manager.is_internal(server_name):
454
- registry = server._internal_manager.get_registry(server_name)
455
- if registry:
456
- # Check if tool exists before calling - return helpful 404 if not
457
- if not registry.get_schema(tool_name):
458
- available = [t["name"] for t in registry.list_tools()]
459
- raise HTTPException(
460
- status_code=404,
461
- detail=f"Tool '{tool_name}' not found on '{server_name}'. "
462
- f"Available: {', '.join(available)}. "
463
- f"Use list_tools(server='{server_name}') to see all tools, "
464
- f"or get_tool_schema(server_name='{server_name}', tool_name='...') for full schema.",
465
- )
466
- try:
467
- result = await registry.call(tool_name, arguments or {})
468
- response_time_ms = (time.perf_counter() - start_time) * 1000
469
- metrics.inc_counter("mcp_tool_calls_succeeded_total")
470
- return {
471
- "success": True,
472
- "result": result,
473
- "response_time_ms": response_time_ms,
474
- }
475
- except Exception as e:
476
- metrics.inc_counter("mcp_tool_calls_failed_total")
477
- error_msg = str(e) or f"{type(e).__name__}: (no message)"
478
- raise HTTPException(
479
- status_code=500,
480
- detail={"success": False, "error": error_msg},
481
- ) from e
482
-
483
- if server.mcp_manager is None:
484
- raise HTTPException(status_code=503, detail="MCP manager not available")
485
-
486
- # Call external MCP tool
487
- try:
488
- result = await server.mcp_manager.call_tool(server_name, tool_name, arguments)
489
- response_time_ms = (time.perf_counter() - start_time) * 1000
490
- metrics.inc_counter("mcp_tool_calls_succeeded_total")
491
-
492
- return {
493
- "success": True,
494
- "result": result,
495
- "response_time_ms": response_time_ms,
496
- }
497
-
498
- except Exception as e:
499
- metrics.inc_counter("mcp_tool_calls_failed_total")
500
- error_msg = str(e) or f"{type(e).__name__}: (no message)"
501
- raise HTTPException(status_code=500, detail=error_msg) from e
502
-
503
- except HTTPException:
504
- raise
505
- except Exception as e:
506
- metrics.inc_counter("mcp_tool_calls_failed_total")
507
- error_msg = str(e) or f"{type(e).__name__}: (no message)"
508
- logger.error(f"Call MCP tool error: {error_msg}", exc_info=True)
509
- raise HTTPException(status_code=500, detail=error_msg) from e
510
-
511
- @router.post("/servers")
512
- async def add_mcp_server(
513
- request: Request,
514
- server: "HTTPServer" = Depends(get_server),
515
- ) -> dict[str, Any]:
516
- """
517
- Add a new MCP server configuration.
518
-
519
- Request body:
520
- {
521
- "name": "my-server",
522
- "transport": "http",
523
- "url": "https://...",
524
- "enabled": true
525
- }
526
-
527
- Returns:
528
- Success status
529
- """
530
- metrics.inc_counter("http_requests_total")
531
-
532
- try:
533
- body = await request.json()
534
- name = body.get("name")
535
- transport = body.get("transport")
536
-
537
- if not name or not transport:
538
- raise HTTPException(status_code=400, detail="Required fields: name, transport")
539
-
540
- # Import here to avoid circular imports
541
- from gobby.mcp_proxy.models import MCPServerConfig
542
- from gobby.utils.project_context import get_project_context
543
-
544
- project_ctx = get_project_context()
545
- if not project_ctx or not project_ctx.get("id"):
546
- raise HTTPException(
547
- status_code=400, detail="No current project found. Run 'gobby init'."
548
- )
549
- project_id = project_ctx["id"]
550
-
551
- config = MCPServerConfig(
552
- name=name,
553
- project_id=project_id,
554
- transport=transport,
555
- url=body.get("url"),
556
- command=body.get("command"),
557
- args=body.get("args"),
558
- env=body.get("env"),
559
- headers=body.get("headers"),
560
- enabled=body.get("enabled", True),
561
- )
562
-
563
- if server.mcp_manager is None:
564
- raise HTTPException(status_code=503, detail="MCP manager not available")
565
-
566
- await server.mcp_manager.add_server(config)
567
-
568
- return {
569
- "success": True,
570
- "message": f"Added MCP server: {name}",
571
- }
572
-
573
- except ValueError as e:
574
- raise HTTPException(status_code=400, detail=str(e)) from e
575
- except HTTPException:
576
- raise
577
- except Exception as e:
578
- metrics.inc_counter("http_requests_errors_total")
579
- logger.error(f"Add MCP server error: {e}", exc_info=True)
580
- raise HTTPException(status_code=500, detail=str(e)) from e
581
-
582
- @router.post("/servers/import")
583
- async def import_mcp_server(
584
- request: Request,
585
- server: "HTTPServer" = Depends(get_server),
586
- ) -> dict[str, Any]:
587
- """
588
- Import MCP server(s) from various sources.
589
-
590
- Request body:
591
- {
592
- "from_project": "other-project", # Import from project
593
- "github_url": "https://...", # Import from GitHub
594
- "query": "supabase mcp", # Search and import
595
- "servers": ["name1", "name2"] # Specific servers to import
596
- }
597
-
598
- Returns:
599
- Import result with imported/skipped/failed lists
600
- """
601
- metrics.inc_counter("http_requests_total")
602
-
603
- try:
604
- body = await request.json()
605
- from_project = body.get("from_project")
606
- github_url = body.get("github_url")
607
- query = body.get("query")
608
- servers = body.get("servers")
609
-
610
- if not from_project and not github_url and not query:
611
- raise HTTPException(
612
- status_code=400,
613
- detail="Specify at least one: from_project, github_url, or query",
614
- )
615
-
616
- # Get current project ID from context
617
- from gobby.utils.project_context import get_project_context
618
-
619
- project_ctx = get_project_context()
620
- if not project_ctx or not project_ctx.get("id"):
621
- raise HTTPException(
622
- status_code=400, detail="No current project. Run 'gobby init' first."
623
- )
624
- current_project_id = project_ctx["id"]
625
-
626
- if not server.config:
627
- raise HTTPException(status_code=500, detail="Daemon configuration not available")
628
-
629
- # Create importer
630
- from gobby.mcp_proxy.importer import MCPServerImporter
631
- from gobby.storage.database import LocalDatabase
632
-
633
- db = LocalDatabase()
634
- importer = MCPServerImporter(
635
- config=server.config,
636
- db=db,
637
- current_project_id=current_project_id,
638
- mcp_client_manager=server.mcp_manager,
639
- )
640
-
641
- # Execute import based on source
642
- if from_project:
643
- result = await importer.import_from_project(
644
- source_project=from_project,
645
- servers=servers,
646
- )
647
- elif github_url:
648
- result = await importer.import_from_github(github_url)
649
- elif query:
650
- result = await importer.import_from_query(query)
651
- else:
652
- result = {"success": False, "error": "No import source specified"}
653
-
654
- return result
655
-
656
- except HTTPException:
657
- raise
658
- except Exception as e:
659
- metrics.inc_counter("http_requests_errors_total")
660
- logger.error(f"Import MCP server error: {e}", exc_info=True)
661
- raise HTTPException(status_code=500, detail=str(e)) from e
662
-
663
- @router.delete("/servers/{name}")
664
- async def remove_mcp_server(
665
- name: str,
666
- server: "HTTPServer" = Depends(get_server),
667
- ) -> dict[str, Any]:
668
- """
669
- Remove an MCP server configuration.
670
-
671
- Args:
672
- name: Server name to remove
673
-
674
- Returns:
675
- Success status
676
- """
677
- metrics.inc_counter("http_requests_total")
678
-
679
- try:
680
- if server.mcp_manager is None:
681
- raise HTTPException(status_code=503, detail="MCP manager not available")
682
-
683
- await server.mcp_manager.remove_server(name)
684
-
685
- return {
686
- "success": True,
687
- "message": f"Removed MCP server: {name}",
688
- }
689
-
690
- except ValueError as e:
691
- raise HTTPException(status_code=404, detail=str(e)) from e
692
- except Exception as e:
693
- metrics.inc_counter("http_requests_errors_total")
694
- logger.error(f"Remove MCP server error: {e}", exc_info=True)
695
- raise HTTPException(status_code=500, detail=str(e)) from e
696
-
697
- @router.post("/tools/recommend")
698
- async def recommend_mcp_tools(
699
- request: Request,
700
- server: "HTTPServer" = Depends(get_server),
701
- ) -> dict[str, Any]:
702
- """
703
- Get AI-powered tool recommendations for a task.
704
-
705
- Request body:
706
- {
707
- "task_description": "I need to query a database",
708
- "agent_id": "optional-agent-id",
709
- "search_mode": "llm" | "semantic" | "hybrid",
710
- "top_k": 10,
711
- "min_similarity": 0.3,
712
- "cwd": "/path/to/project"
713
- }
714
-
715
- Returns:
716
- List of tool recommendations
717
- """
718
- start_time = time.perf_counter()
719
- metrics.inc_counter("http_requests_total")
720
-
721
- try:
722
- body = await request.json()
723
- task_description = body.get("task_description")
724
- agent_id = body.get("agent_id")
725
- search_mode = body.get("search_mode", "llm")
726
- top_k = body.get("top_k", 10)
727
- min_similarity = body.get("min_similarity", 0.3)
728
- cwd = body.get("cwd")
729
-
730
- if not task_description:
731
- raise HTTPException(status_code=400, detail="Required field: task_description")
732
-
733
- # For semantic/hybrid modes, resolve project_id from cwd
734
- project_id = None
735
- if search_mode in ("semantic", "hybrid"):
736
- try:
737
- project_id = server._resolve_project_id(None, cwd)
738
- except ValueError as e:
739
- return {
740
- "success": False,
741
- "error": str(e),
742
- "task": task_description,
743
- "response_time_ms": (time.perf_counter() - start_time) * 1000,
744
- }
745
-
746
- # Use tools handler if available
747
- if server._tools_handler:
748
- result = await server._tools_handler.recommend_tools(
749
- task_description=task_description,
750
- agent_id=agent_id,
751
- search_mode=search_mode,
752
- top_k=top_k,
753
- min_similarity=min_similarity,
754
- project_id=project_id,
755
- )
756
- response_time_ms = (time.perf_counter() - start_time) * 1000
757
- result["response_time_ms"] = response_time_ms
758
- return result
759
-
760
- # Fallback: no tools handler
761
- return {
762
- "success": False,
763
- "error": "Tools handler not initialized",
764
- "recommendations": [],
765
- "response_time_ms": (time.perf_counter() - start_time) * 1000,
766
- }
767
-
768
- except HTTPException:
769
- raise
770
- except Exception as e:
771
- metrics.inc_counter("http_requests_errors_total")
772
- logger.error(f"Recommend tools error: {e}", exc_info=True)
773
- raise HTTPException(status_code=500, detail=str(e)) from e
774
-
775
- @router.post("/tools/search")
776
- async def search_mcp_tools(
777
- request: Request,
778
- server: "HTTPServer" = Depends(get_server),
779
- ) -> dict[str, Any]:
780
- """
781
- Search for tools using semantic similarity.
782
-
783
- Request body:
784
- {
785
- "query": "create a file",
786
- "top_k": 10,
787
- "min_similarity": 0.0,
788
- "server": "optional-server-filter",
789
- "cwd": "/path/to/project"
790
- }
791
-
792
- Returns:
793
- List of matching tools with similarity scores
794
- """
795
- start_time = time.perf_counter()
796
- metrics.inc_counter("http_requests_total")
797
-
798
- try:
799
- body = await request.json()
800
- query = body.get("query")
801
- top_k = body.get("top_k", 10)
802
- min_similarity = body.get("min_similarity", 0.0)
803
- server_filter = body.get("server")
804
- cwd = body.get("cwd")
805
-
806
- if not query:
807
- raise HTTPException(status_code=400, detail="Required field: query")
808
-
809
- # Resolve project_id from cwd
810
- try:
811
- project_id = server._resolve_project_id(None, cwd)
812
- except ValueError as e:
813
- return {
814
- "success": False,
815
- "error": str(e),
816
- "query": query,
817
- "response_time_ms": (time.perf_counter() - start_time) * 1000,
818
- }
819
-
820
- # Use semantic search directly if available
821
- if server._tools_handler and server._tools_handler._semantic_search:
822
- try:
823
- semantic_search = server._tools_handler._semantic_search
824
-
825
- # Auto-generate embeddings if none exist
826
- existing = semantic_search.get_embeddings_for_project(project_id)
827
- if not existing and server._mcp_db_manager:
828
- logger.info(f"No embeddings for project {project_id}, generating...")
829
- await semantic_search.embed_all_tools(
830
- project_id=project_id,
831
- mcp_manager=server._mcp_db_manager,
832
- force=False,
833
- )
834
-
835
- results = await semantic_search.search_tools(
836
- query=query,
837
- project_id=project_id,
838
- top_k=top_k,
839
- min_similarity=min_similarity,
840
- server_filter=server_filter,
841
- )
842
- response_time_ms = (time.perf_counter() - start_time) * 1000
843
- return {
844
- "success": True,
845
- "query": query,
846
- "results": [r.to_dict() for r in results],
847
- "total_results": len(results),
848
- "response_time_ms": response_time_ms,
849
- }
850
- except Exception as e:
851
- logger.error(f"Semantic search failed: {e}")
852
- return {
853
- "success": False,
854
- "error": str(e),
855
- "query": query,
856
- "response_time_ms": (time.perf_counter() - start_time) * 1000,
857
- }
858
-
859
- # Fallback: no semantic search
860
- return {
861
- "success": False,
862
- "error": "Semantic search not configured",
863
- "results": [],
864
- "response_time_ms": (time.perf_counter() - start_time) * 1000,
865
- }
866
-
867
- except HTTPException:
868
- raise
869
- except Exception as e:
870
- metrics.inc_counter("http_requests_errors_total")
871
- logger.error(f"Search tools error: {e}", exc_info=True)
872
- raise HTTPException(status_code=500, detail=str(e)) from e
873
-
874
- @router.post("/tools/embed")
875
- async def embed_mcp_tools(
876
- request: Request,
877
- server: "HTTPServer" = Depends(get_server),
878
- ) -> dict[str, Any]:
879
- """
880
- Generate embeddings for all tools in a project.
881
-
882
- Request body:
883
- {
884
- "cwd": "/path/to/project",
885
- "force": false
886
- }
887
-
888
- Returns:
889
- Embedding generation stats
890
- """
891
- start_time = time.perf_counter()
892
- metrics.inc_counter("http_requests_total")
893
-
894
- try:
895
- body = await request.json()
896
- cwd = body.get("cwd")
897
- force = body.get("force", False)
898
-
899
- # Resolve project_id from cwd
900
- try:
901
- project_id = server._resolve_project_id(None, cwd)
902
- except ValueError as e:
903
- return {
904
- "success": False,
905
- "error": str(e),
906
- "response_time_ms": (time.perf_counter() - start_time) * 1000,
907
- }
908
-
909
- # Use semantic search to embed all tools
910
- if server._tools_handler and server._tools_handler._semantic_search:
911
- try:
912
- stats = await server._tools_handler._semantic_search.embed_all_tools(
913
- project_id=project_id,
914
- mcp_manager=server._mcp_db_manager,
915
- force=force,
916
- )
917
- response_time_ms = (time.perf_counter() - start_time) * 1000
918
- return {
919
- "success": True,
920
- "stats": stats,
921
- "response_time_ms": response_time_ms,
922
- }
923
- except Exception as e:
924
- logger.error(f"Embedding generation failed: {e}")
925
- return {
926
- "success": False,
927
- "error": str(e),
928
- "response_time_ms": (time.perf_counter() - start_time) * 1000,
929
- }
930
-
931
- return {
932
- "success": False,
933
- "error": "Semantic search not configured",
934
- "response_time_ms": (time.perf_counter() - start_time) * 1000,
935
- }
936
-
937
- except HTTPException:
938
- raise
939
- except Exception as e:
940
- metrics.inc_counter("http_requests_errors_total")
941
- logger.error(f"Embed tools error: {e}", exc_info=True)
942
- raise HTTPException(status_code=500, detail=str(e)) from e
943
-
944
- @router.get("/status")
945
- async def get_mcp_status(
946
- server: "HTTPServer" = Depends(get_server),
947
- ) -> dict[str, Any]:
948
- """
949
- Get MCP proxy status and health.
950
-
951
- Returns:
952
- Status summary with server counts and health info
953
- """
954
- start_time = time.perf_counter()
955
- metrics.inc_counter("http_requests_total")
956
-
957
- try:
958
- total_servers = 0
959
- connected_servers = 0
960
- cached_tools = 0
961
- server_health: dict[str, dict[str, Any]] = {}
962
-
963
- # Count internal servers
964
- if server._internal_manager:
965
- for registry in server._internal_manager.get_all_registries():
966
- total_servers += 1
967
- connected_servers += 1
968
- cached_tools += len(registry.list_tools())
969
- server_health[registry.name] = {
970
- "state": "connected",
971
- "health": "healthy",
972
- "failures": 0,
973
- }
974
-
975
- # Count external servers
976
- if server.mcp_manager:
977
- for config in server.mcp_manager.server_configs:
978
- total_servers += 1
979
- health = server.mcp_manager.health.get(config.name)
980
- is_connected = config.name in server.mcp_manager.connections
981
- if is_connected:
982
- connected_servers += 1
983
-
984
- server_health[config.name] = {
985
- "state": health.state.value if health else "unknown",
986
- "health": health.health.value if health else "unknown",
987
- "failures": health.consecutive_failures if health else 0,
988
- }
989
-
990
- response_time_ms = (time.perf_counter() - start_time) * 1000
991
-
992
- return {
993
- "total_servers": total_servers,
994
- "connected_servers": connected_servers,
995
- "cached_tools": cached_tools,
996
- "server_health": server_health,
997
- "response_time_ms": response_time_ms,
998
- }
999
-
1000
- except Exception as e:
1001
- metrics.inc_counter("http_requests_errors_total")
1002
- logger.error(f"Get MCP status error: {e}", exc_info=True)
1003
- raise HTTPException(status_code=500, detail=str(e)) from e
1004
-
1005
- @router.post("/{server_name}/tools/{tool_name}")
1006
- async def mcp_proxy(
1007
- server_name: str,
1008
- tool_name: str,
1009
- request: Request,
1010
- server: "HTTPServer" = Depends(get_server),
1011
- ) -> dict[str, Any]:
1012
- """
1013
- Unified MCP proxy endpoint for calling MCP server tools.
1014
-
1015
- Args:
1016
- server_name: Name of the MCP server
1017
- tool_name: Name of the tool to call
1018
- request: FastAPI request with tool arguments in body
1019
-
1020
- Returns:
1021
- Tool execution result
1022
- """
1023
- start_time = time.perf_counter()
1024
- metrics.inc_counter("http_requests_total")
1025
- metrics.inc_counter("mcp_tool_calls_total")
1026
-
1027
- try:
1028
- # Parse request body as tool arguments
1029
- try:
1030
- args = await request.json()
1031
- except (json.JSONDecodeError, ValueError) as e:
1032
- raise HTTPException(
1033
- status_code=400, detail=f"Invalid JSON in request body: {e}"
1034
- ) from e
1035
-
1036
- # Check internal registries first (gobby-tasks, gobby-memory, etc.)
1037
- if server._internal_manager and server._internal_manager.is_internal(server_name):
1038
- registry = server._internal_manager.get_registry(server_name)
1039
- if registry:
1040
- # Check if tool exists before calling - return helpful 404 if not
1041
- if not registry.get_schema(tool_name):
1042
- available = [t["name"] for t in registry.list_tools()]
1043
- raise HTTPException(
1044
- status_code=404,
1045
- detail=f"Tool '{tool_name}' not found on '{server_name}'. "
1046
- f"Available: {', '.join(available)}. "
1047
- f"Use list_tools(server='{server_name}') to see all tools, "
1048
- f"or get_tool_schema(server_name='{server_name}', tool_name='...') for full schema.",
1049
- )
1050
- try:
1051
- result = await registry.call(tool_name, args or {})
1052
- response_time_ms = (time.perf_counter() - start_time) * 1000
1053
- metrics.inc_counter("mcp_tool_calls_succeeded_total")
1054
- return {
1055
- "success": True,
1056
- "result": result,
1057
- "response_time_ms": response_time_ms,
1058
- }
1059
- except Exception as e:
1060
- metrics.inc_counter("mcp_tool_calls_failed_total")
1061
- error_msg = str(e) or f"{type(e).__name__}: (no message)"
1062
- raise HTTPException(status_code=500, detail=error_msg) from e
1063
- raise HTTPException(
1064
- status_code=404, detail=f"Internal server '{server_name}' not found"
1065
- )
1066
-
1067
- if server.mcp_manager is None:
1068
- raise HTTPException(status_code=503, detail="MCP manager not available")
1069
-
1070
- # Call MCP tool
1071
- try:
1072
- result = await server.mcp_manager.call_tool(server_name, tool_name, args)
1073
-
1074
- response_time_ms = (time.perf_counter() - start_time) * 1000
1075
-
1076
- logger.debug(
1077
- f"MCP tool call successful: {server_name}.{tool_name}",
1078
- extra={
1079
- "server": server_name,
1080
- "tool": tool_name,
1081
- "response_time_ms": response_time_ms,
1082
- },
1083
- )
1084
-
1085
- metrics.inc_counter("mcp_tool_calls_succeeded_total")
1086
-
1087
- return {
1088
- "success": True,
1089
- "result": result,
1090
- "response_time_ms": response_time_ms,
1091
- }
1092
-
1093
- except ValueError as e:
1094
- metrics.inc_counter("mcp_tool_calls_failed_total")
1095
- logger.warning(
1096
- f"MCP tool not found: {server_name}.{tool_name}",
1097
- extra={"server": server_name, "tool": tool_name, "error": str(e)},
1098
- )
1099
- raise HTTPException(status_code=404, detail=str(e)) from e
1100
- except Exception as e:
1101
- metrics.inc_counter("mcp_tool_calls_failed_total")
1102
- error_msg = str(e) or f"{type(e).__name__}: (no message)"
1103
- logger.error(
1104
- f"MCP tool call error: {server_name}.{tool_name}",
1105
- exc_info=True,
1106
- extra={"server": server_name, "tool": tool_name},
1107
- )
1108
- raise HTTPException(status_code=500, detail=error_msg) from e
1109
-
1110
- except HTTPException:
1111
- raise
1112
- except Exception as e:
1113
- metrics.inc_counter("mcp_tool_calls_failed_total")
1114
- error_msg = str(e) or f"{type(e).__name__}: (no message)"
1115
- logger.error(f"MCP proxy error: {server_name}.{tool_name}", exc_info=True)
1116
- raise HTTPException(status_code=500, detail=error_msg) from e
1117
-
1118
- @router.post("/refresh")
1119
- async def refresh_mcp_tools(
1120
- request: Request,
1121
- server: "HTTPServer" = Depends(get_server),
1122
- ) -> dict[str, Any]:
1123
- """
1124
- Refresh MCP tools - detect schema changes and re-index as needed.
1125
-
1126
- Request body:
1127
- {
1128
- "cwd": "/path/to/project",
1129
- "force": false,
1130
- "server": "optional-server-filter"
1131
- }
1132
-
1133
- Returns:
1134
- Refresh stats with new/changed/unchanged tool counts
1135
- """
1136
- start_time = time.perf_counter()
1137
- metrics.inc_counter("http_requests_total")
1138
-
1139
- try:
1140
- body = await request.json()
1141
- cwd = body.get("cwd")
1142
- force = body.get("force", False)
1143
- server_filter = body.get("server")
1144
-
1145
- # Resolve project_id from cwd
1146
- try:
1147
- project_id = server._resolve_project_id(None, cwd)
1148
- except ValueError as e:
1149
- return {
1150
- "success": False,
1151
- "error": str(e),
1152
- "response_time_ms": (time.perf_counter() - start_time) * 1000,
1153
- }
1154
-
1155
- # Need schema hash manager and semantic search
1156
- if not server._mcp_db_manager:
1157
- return {
1158
- "success": False,
1159
- "error": "MCP database manager not configured",
1160
- "response_time_ms": (time.perf_counter() - start_time) * 1000,
1161
- }
1162
-
1163
- from gobby.mcp_proxy.schema_hash import SchemaHashManager, compute_schema_hash
1164
-
1165
- schema_hash_manager = SchemaHashManager(db=server._mcp_db_manager.db)
1166
- semantic_search = (
1167
- getattr(server._tools_handler, "_semantic_search", None)
1168
- if server._tools_handler
1169
- else None
1170
- )
1171
-
1172
- stats: dict[str, Any] = {
1173
- "servers_processed": 0,
1174
- "tools_new": 0,
1175
- "tools_changed": 0,
1176
- "tools_unchanged": 0,
1177
- "tools_removed": 0,
1178
- "embeddings_generated": 0,
1179
- "by_server": {},
1180
- }
1181
-
1182
- # Collect servers to process
1183
- servers_to_process: list[str] = []
1184
-
1185
- # Internal servers
1186
- if server._internal_manager:
1187
- for registry in server._internal_manager.get_all_registries():
1188
- if server_filter is None or registry.name == server_filter:
1189
- servers_to_process.append(registry.name)
1190
-
1191
- # External MCP servers
1192
- if server.mcp_manager:
1193
- for config in server.mcp_manager.server_configs:
1194
- if config.enabled:
1195
- if server_filter is None or config.name == server_filter:
1196
- servers_to_process.append(config.name)
1197
-
1198
- # Process each server
1199
- for server_name in servers_to_process:
1200
- try:
1201
- tools: list[dict[str, Any]] = []
1202
-
1203
- # Get tools from internal or external server
1204
- if server._internal_manager and server._internal_manager.is_internal(
1205
- server_name
1206
- ):
1207
- internal_registry = server._internal_manager.get_registry(server_name)
1208
- if internal_registry:
1209
- for t in internal_registry.list_tools():
1210
- tool_name = t.get("name", "")
1211
- tools.append(
1212
- {
1213
- "name": tool_name,
1214
- "description": t.get("description"),
1215
- "inputSchema": internal_registry.get_schema(tool_name),
1216
- }
1217
- )
1218
- elif server.mcp_manager:
1219
- try:
1220
- session = await server.mcp_manager.ensure_connected(server_name)
1221
- tools_result = await session.list_tools()
1222
- for t in tools_result.tools:
1223
- schema = None
1224
- if hasattr(t, "inputSchema"):
1225
- if hasattr(t.inputSchema, "model_dump"):
1226
- schema = t.inputSchema.model_dump()
1227
- elif isinstance(t.inputSchema, dict):
1228
- schema = t.inputSchema
1229
- tools.append(
1230
- {
1231
- "name": getattr(t, "name", ""),
1232
- "description": getattr(t, "description", ""),
1233
- "inputSchema": schema,
1234
- }
1235
- )
1236
- except Exception as e:
1237
- logger.warning(f"Failed to connect to {server_name}: {e}")
1238
- stats["by_server"][server_name] = {"error": str(e)}
1239
- continue
1240
-
1241
- # Check for schema changes
1242
- if force:
1243
- # Force mode: treat all as new
1244
- changes = {
1245
- "new": [t["name"] for t in tools],
1246
- "changed": [],
1247
- "unchanged": [],
1248
- }
1249
- else:
1250
- changes = schema_hash_manager.check_tools_for_changes(
1251
- server_name=server_name,
1252
- project_id=project_id,
1253
- tools=tools,
1254
- )
1255
-
1256
- server_stats = {
1257
- "new": len(changes["new"]),
1258
- "changed": len(changes["changed"]),
1259
- "unchanged": len(changes["unchanged"]),
1260
- "removed": 0,
1261
- "embeddings": 0,
1262
- }
1263
-
1264
- # Update schema hashes for new/changed tools
1265
- tools_to_embed = []
1266
- for tool in tools:
1267
- tool_name = tool["name"]
1268
- if tool_name in changes["new"] or tool_name in changes["changed"]:
1269
- schema = tool.get("inputSchema")
1270
- schema_hash = compute_schema_hash(schema)
1271
- schema_hash_manager.store_hash(
1272
- server_name=server_name,
1273
- tool_name=tool_name,
1274
- project_id=project_id,
1275
- schema_hash=schema_hash,
1276
- )
1277
- tools_to_embed.append(tool)
1278
- else:
1279
- # Just update verification time for unchanged
1280
- schema_hash_manager.update_verification_time(
1281
- server_name=server_name,
1282
- tool_name=tool_name,
1283
- project_id=project_id,
1284
- )
1285
-
1286
- # Clean up stale hashes
1287
- valid_tool_names = [t["name"] for t in tools]
1288
- removed = schema_hash_manager.cleanup_stale_hashes(
1289
- server_name=server_name,
1290
- project_id=project_id,
1291
- valid_tool_names=valid_tool_names,
1292
- )
1293
- server_stats["removed"] = removed
1294
-
1295
- # Generate embeddings for new/changed tools
1296
- if semantic_search and tools_to_embed:
1297
- for tool in tools_to_embed:
1298
- try:
1299
- await semantic_search.embed_tool(
1300
- server_name=server_name,
1301
- tool_name=tool["name"],
1302
- description=tool.get("description", ""),
1303
- input_schema=tool.get("inputSchema"),
1304
- project_id=project_id,
1305
- )
1306
- server_stats["embeddings"] += 1
1307
- except Exception as e:
1308
- logger.warning(f"Failed to embed {server_name}/{tool['name']}: {e}")
1309
-
1310
- stats["by_server"][server_name] = server_stats
1311
- stats["servers_processed"] += 1
1312
- stats["tools_new"] += server_stats["new"]
1313
- stats["tools_changed"] += server_stats["changed"]
1314
- stats["tools_unchanged"] += server_stats["unchanged"]
1315
- stats["tools_removed"] += server_stats["removed"]
1316
- stats["embeddings_generated"] += server_stats["embeddings"]
1317
-
1318
- except Exception as e:
1319
- logger.error(f"Error processing server {server_name}: {e}")
1320
- stats["by_server"][server_name] = {"error": str(e)}
1321
-
1322
- response_time_ms = (time.perf_counter() - start_time) * 1000
1323
- return {
1324
- "success": True,
1325
- "force": force,
1326
- "stats": stats,
1327
- "response_time_ms": response_time_ms,
1328
- }
1329
45
 
1330
- except HTTPException:
1331
- raise
1332
- except Exception as e:
1333
- metrics.inc_counter("http_requests_errors_total")
1334
- logger.error(f"Refresh tools error: {e}", exc_info=True)
1335
- raise HTTPException(status_code=500, detail=str(e)) from e
46
+ # Execution endpoints from endpoints/execution.py
47
+ router.get("/{server_name}/tools")(list_mcp_tools)
48
+ router.post("/tools/schema")(get_tool_schema)
49
+ router.post("/tools/call")(call_mcp_tool)
50
+ router.post("/{server_name}/tools/{tool_name}")(mcp_proxy)
51
+
52
+ # Server management endpoints from endpoints/server.py
53
+ router.get("/servers")(list_mcp_servers)
54
+ router.post("/servers")(add_mcp_server)
55
+ router.post("/servers/import")(import_mcp_server)
56
+ router.delete("/servers/{name}")(remove_mcp_server)
57
+
58
+ # Discovery endpoints from endpoints/discovery.py
59
+ router.get("/tools")(list_all_mcp_tools)
60
+ router.post("/tools/recommend")(recommend_mcp_tools)
61
+ router.post("/tools/search")(search_mcp_tools)
62
+
63
+ # Registry endpoints from endpoints/registry.py
64
+ router.post("/tools/embed")(embed_mcp_tools)
65
+ router.get("/status")(get_mcp_status)
66
+ router.post("/refresh")(refresh_mcp_tools)
1336
67
 
1337
68
  return router