gobby 0.2.6__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 (146) hide show
  1. gobby/__init__.py +1 -1
  2. gobby/adapters/__init__.py +2 -1
  3. gobby/adapters/codex_impl/__init__.py +28 -0
  4. gobby/adapters/codex_impl/adapter.py +722 -0
  5. gobby/adapters/codex_impl/client.py +679 -0
  6. gobby/adapters/codex_impl/protocol.py +20 -0
  7. gobby/adapters/codex_impl/types.py +68 -0
  8. gobby/agents/definitions.py +11 -1
  9. gobby/agents/isolation.py +395 -0
  10. gobby/agents/sandbox.py +261 -0
  11. gobby/agents/spawn.py +42 -287
  12. gobby/agents/spawn_executor.py +385 -0
  13. gobby/agents/spawners/__init__.py +24 -0
  14. gobby/agents/spawners/command_builder.py +189 -0
  15. gobby/agents/spawners/embedded.py +21 -2
  16. gobby/agents/spawners/headless.py +21 -2
  17. gobby/agents/spawners/prompt_manager.py +125 -0
  18. gobby/cli/install.py +4 -4
  19. gobby/cli/installers/claude.py +6 -0
  20. gobby/cli/installers/gemini.py +6 -0
  21. gobby/cli/installers/shared.py +103 -4
  22. gobby/cli/sessions.py +1 -1
  23. gobby/cli/utils.py +9 -2
  24. gobby/config/__init__.py +12 -97
  25. gobby/config/app.py +10 -94
  26. gobby/config/extensions.py +2 -2
  27. gobby/config/features.py +7 -130
  28. gobby/config/tasks.py +4 -28
  29. gobby/hooks/__init__.py +0 -13
  30. gobby/hooks/event_handlers.py +45 -2
  31. gobby/hooks/hook_manager.py +2 -2
  32. gobby/hooks/plugins.py +1 -1
  33. gobby/hooks/webhooks.py +1 -1
  34. gobby/llm/resolver.py +3 -2
  35. gobby/mcp_proxy/importer.py +62 -4
  36. gobby/mcp_proxy/instructions.py +2 -0
  37. gobby/mcp_proxy/registries.py +1 -4
  38. gobby/mcp_proxy/services/recommendation.py +43 -11
  39. gobby/mcp_proxy/tools/agents.py +31 -731
  40. gobby/mcp_proxy/tools/clones.py +0 -385
  41. gobby/mcp_proxy/tools/memory.py +2 -2
  42. gobby/mcp_proxy/tools/sessions/__init__.py +14 -0
  43. gobby/mcp_proxy/tools/sessions/_commits.py +232 -0
  44. gobby/mcp_proxy/tools/sessions/_crud.py +253 -0
  45. gobby/mcp_proxy/tools/sessions/_factory.py +63 -0
  46. gobby/mcp_proxy/tools/sessions/_handoff.py +499 -0
  47. gobby/mcp_proxy/tools/sessions/_messages.py +138 -0
  48. gobby/mcp_proxy/tools/skills/__init__.py +14 -29
  49. gobby/mcp_proxy/tools/spawn_agent.py +417 -0
  50. gobby/mcp_proxy/tools/tasks/_lifecycle.py +52 -18
  51. gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +1 -1
  52. gobby/mcp_proxy/tools/worktrees.py +0 -343
  53. gobby/memory/ingestion/__init__.py +5 -0
  54. gobby/memory/ingestion/multimodal.py +221 -0
  55. gobby/memory/manager.py +62 -283
  56. gobby/memory/search/__init__.py +10 -0
  57. gobby/memory/search/coordinator.py +248 -0
  58. gobby/memory/services/__init__.py +5 -0
  59. gobby/memory/services/crossref.py +142 -0
  60. gobby/prompts/loader.py +5 -2
  61. gobby/servers/http.py +1 -4
  62. gobby/servers/routes/admin.py +14 -0
  63. gobby/servers/routes/mcp/endpoints/__init__.py +61 -0
  64. gobby/servers/routes/mcp/endpoints/discovery.py +405 -0
  65. gobby/servers/routes/mcp/endpoints/execution.py +568 -0
  66. gobby/servers/routes/mcp/endpoints/registry.py +378 -0
  67. gobby/servers/routes/mcp/endpoints/server.py +304 -0
  68. gobby/servers/routes/mcp/hooks.py +1 -1
  69. gobby/servers/routes/mcp/tools.py +48 -1506
  70. gobby/sessions/lifecycle.py +1 -1
  71. gobby/sessions/processor.py +10 -0
  72. gobby/sessions/transcripts/base.py +1 -0
  73. gobby/sessions/transcripts/claude.py +15 -5
  74. gobby/skills/parser.py +30 -2
  75. gobby/storage/migrations.py +159 -372
  76. gobby/storage/sessions.py +43 -7
  77. gobby/storage/skills.py +37 -4
  78. gobby/storage/tasks/_lifecycle.py +18 -3
  79. gobby/sync/memories.py +1 -1
  80. gobby/tasks/external_validator.py +1 -1
  81. gobby/tasks/validation.py +22 -20
  82. gobby/tools/summarizer.py +91 -10
  83. gobby/utils/project_context.py +2 -3
  84. gobby/utils/status.py +13 -0
  85. gobby/workflows/actions.py +221 -1217
  86. gobby/workflows/artifact_actions.py +31 -0
  87. gobby/workflows/autonomous_actions.py +11 -0
  88. gobby/workflows/context_actions.py +50 -1
  89. gobby/workflows/enforcement/__init__.py +47 -0
  90. gobby/workflows/enforcement/blocking.py +269 -0
  91. gobby/workflows/enforcement/commit_policy.py +283 -0
  92. gobby/workflows/enforcement/handlers.py +269 -0
  93. gobby/workflows/enforcement/task_policy.py +542 -0
  94. gobby/workflows/git_utils.py +106 -0
  95. gobby/workflows/llm_actions.py +30 -0
  96. gobby/workflows/mcp_actions.py +20 -1
  97. gobby/workflows/memory_actions.py +80 -0
  98. gobby/workflows/safe_evaluator.py +183 -0
  99. gobby/workflows/session_actions.py +44 -0
  100. gobby/workflows/state_actions.py +60 -1
  101. gobby/workflows/stop_signal_actions.py +55 -0
  102. gobby/workflows/summary_actions.py +94 -1
  103. gobby/workflows/task_sync_actions.py +347 -0
  104. gobby/workflows/todo_actions.py +34 -1
  105. gobby/workflows/webhook_actions.py +185 -0
  106. {gobby-0.2.6.dist-info → gobby-0.2.7.dist-info}/METADATA +6 -1
  107. {gobby-0.2.6.dist-info → gobby-0.2.7.dist-info}/RECORD +111 -111
  108. {gobby-0.2.6.dist-info → gobby-0.2.7.dist-info}/WHEEL +1 -1
  109. gobby/adapters/codex.py +0 -1332
  110. gobby/install/claude/commands/gobby/bug.md +0 -51
  111. gobby/install/claude/commands/gobby/chore.md +0 -51
  112. gobby/install/claude/commands/gobby/epic.md +0 -52
  113. gobby/install/claude/commands/gobby/eval.md +0 -235
  114. gobby/install/claude/commands/gobby/feat.md +0 -49
  115. gobby/install/claude/commands/gobby/nit.md +0 -52
  116. gobby/install/claude/commands/gobby/ref.md +0 -52
  117. gobby/mcp_proxy/tools/session_messages.py +0 -1055
  118. gobby/prompts/defaults/expansion/system.md +0 -119
  119. gobby/prompts/defaults/expansion/user.md +0 -48
  120. gobby/prompts/defaults/external_validation/agent.md +0 -72
  121. gobby/prompts/defaults/external_validation/external.md +0 -63
  122. gobby/prompts/defaults/external_validation/spawn.md +0 -83
  123. gobby/prompts/defaults/external_validation/system.md +0 -6
  124. gobby/prompts/defaults/features/import_mcp.md +0 -22
  125. gobby/prompts/defaults/features/import_mcp_github.md +0 -17
  126. gobby/prompts/defaults/features/import_mcp_search.md +0 -16
  127. gobby/prompts/defaults/features/recommend_tools.md +0 -32
  128. gobby/prompts/defaults/features/recommend_tools_hybrid.md +0 -35
  129. gobby/prompts/defaults/features/recommend_tools_llm.md +0 -30
  130. gobby/prompts/defaults/features/server_description.md +0 -20
  131. gobby/prompts/defaults/features/server_description_system.md +0 -6
  132. gobby/prompts/defaults/features/task_description.md +0 -31
  133. gobby/prompts/defaults/features/task_description_system.md +0 -6
  134. gobby/prompts/defaults/features/tool_summary.md +0 -17
  135. gobby/prompts/defaults/features/tool_summary_system.md +0 -6
  136. gobby/prompts/defaults/handoff/compact.md +0 -63
  137. gobby/prompts/defaults/handoff/session_end.md +0 -57
  138. gobby/prompts/defaults/memory/extract.md +0 -61
  139. gobby/prompts/defaults/research/step.md +0 -58
  140. gobby/prompts/defaults/validation/criteria.md +0 -47
  141. gobby/prompts/defaults/validation/validate.md +0 -38
  142. gobby/storage/migrations_legacy.py +0 -1359
  143. gobby/workflows/task_enforcement_actions.py +0 -1343
  144. {gobby-0.2.6.dist-info → gobby-0.2.7.dist-info}/entry_points.txt +0 -0
  145. {gobby-0.2.6.dist-info → gobby-0.2.7.dist-info}/licenses/LICENSE.md +0 -0
  146. {gobby-0.2.6.dist-info → gobby-0.2.7.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,568 @@
1
+ """
2
+ Execution endpoints for MCP tool invocation.
3
+
4
+ Extracted from tools.py as part of Phase 2 Strangler Fig decomposition.
5
+ These endpoints handle tool listing, schema retrieval, and tool execution.
6
+ """
7
+
8
+ import json
9
+ import logging
10
+ import re
11
+ import time
12
+ from typing import TYPE_CHECKING, Any
13
+
14
+ from fastapi import Depends, HTTPException, Request
15
+
16
+ from gobby.servers.routes.dependencies import get_internal_manager, get_mcp_manager, get_server
17
+ from gobby.utils.metrics import get_metrics_collector
18
+
19
+ if TYPE_CHECKING:
20
+ from gobby.mcp_proxy.manager import MCPClientManager
21
+ from gobby.mcp_proxy.registry_manager import InternalToolRegistryManager
22
+ from gobby.servers.http import HTTPServer
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+ # Module-level metrics collector (shared across all requests)
27
+ _metrics = get_metrics_collector()
28
+
29
+
30
+ def _process_tool_proxy_result(
31
+ result: Any,
32
+ server_name: str,
33
+ tool_name: str,
34
+ response_time_ms: float,
35
+ ) -> dict[str, Any]:
36
+ """
37
+ Process tool proxy result with consistent metrics, logging, and error handling.
38
+
39
+ Args:
40
+ result: The result from tool_proxy.call_tool()
41
+ server_name: Name of the MCP server
42
+ tool_name: Name of the tool called
43
+ response_time_ms: Response time in milliseconds
44
+
45
+ Returns:
46
+ Wrapped result dict with success status and response time
47
+
48
+ Raises:
49
+ HTTPException: 404 if server not found/not configured
50
+ """
51
+ # Track metrics for tool-level failures vs successes
52
+ if isinstance(result, dict) and result.get("success") is False:
53
+ _metrics.inc_counter("mcp_tool_calls_failed_total")
54
+
55
+ # Check structured error code first (preferred)
56
+ error_code = result.get("error_code")
57
+ if error_code in ("SERVER_NOT_FOUND", "SERVER_NOT_CONFIGURED"):
58
+ # Normalize result to standard error shape while preserving existing fields
59
+ normalized = {"success": False, "error": result.get("error", "Unknown error")}
60
+ for key, value in result.items():
61
+ if key not in normalized:
62
+ normalized[key] = value
63
+ raise HTTPException(status_code=404, detail=normalized)
64
+
65
+ # Backward compatibility: fall back to regex matching if no error_code
66
+ if not error_code:
67
+ logger.debug(
68
+ "ToolProxyService returned error without error_code - using regex fallback"
69
+ )
70
+ error_msg = str(result.get("error", ""))
71
+ if re.search(r"server\s+(not\s+found|not\s+configured)", error_msg, re.IGNORECASE):
72
+ normalized = {"success": False, "error": result.get("error", "Unknown error")}
73
+ for key, value in result.items():
74
+ if key not in normalized:
75
+ normalized[key] = value
76
+ raise HTTPException(status_code=404, detail=normalized)
77
+
78
+ # Tool-level failure (not a transport error) - return failure envelope
79
+ return {
80
+ "success": False,
81
+ "result": result,
82
+ "response_time_ms": response_time_ms,
83
+ }
84
+ else:
85
+ _metrics.inc_counter("mcp_tool_calls_succeeded_total")
86
+ logger.debug(
87
+ f"MCP tool call successful: {server_name}.{tool_name}",
88
+ extra={
89
+ "server": server_name,
90
+ "tool": tool_name,
91
+ "response_time_ms": response_time_ms,
92
+ },
93
+ )
94
+
95
+ # Return 200 with wrapped result for success cases
96
+ return {
97
+ "success": True,
98
+ "result": result,
99
+ "response_time_ms": response_time_ms,
100
+ }
101
+
102
+
103
+ async def _call_internal_tool(
104
+ registry: Any,
105
+ server_name: str,
106
+ tool_name: str,
107
+ arguments: dict[str, Any] | None,
108
+ start_time: float,
109
+ ) -> dict[str, Any]:
110
+ """Shared helper for calling internal registry tools.
111
+
112
+ Args:
113
+ registry: The internal tool registry
114
+ server_name: Name of the MCP server
115
+ tool_name: Name of the tool to call
116
+ arguments: Arguments to pass to the tool
117
+ start_time: Request start time for response_time_ms calculation
118
+
119
+ Returns:
120
+ Tool execution result dict
121
+
122
+ Raises:
123
+ HTTPException: 404 if tool not found, 500 on execution error
124
+ """
125
+ # Check if tool exists before calling - return helpful 404 if not
126
+ if not registry.get_schema(tool_name):
127
+ available = [t["name"] for t in registry.list_tools()]
128
+ raise HTTPException(
129
+ status_code=404,
130
+ detail={
131
+ "success": False,
132
+ "error": f"Tool '{tool_name}' not found on '{server_name}'. "
133
+ f"Available: {', '.join(available)}. "
134
+ f"Use list_tools(server='{server_name}') to see all tools, "
135
+ f"or get_tool_schema(server_name='{server_name}', tool_name='...') for full schema.",
136
+ },
137
+ )
138
+ try:
139
+ result = await registry.call(tool_name, arguments or {})
140
+ response_time_ms = (time.perf_counter() - start_time) * 1000
141
+ _metrics.inc_counter("mcp_tool_calls_succeeded_total")
142
+ return {
143
+ "success": True,
144
+ "result": result,
145
+ "response_time_ms": response_time_ms,
146
+ }
147
+ except Exception as e:
148
+ _metrics.inc_counter("mcp_tool_calls_failed_total")
149
+ error_msg = str(e) or f"{type(e).__name__}: (no message)"
150
+ raise HTTPException(
151
+ status_code=500,
152
+ detail={"success": False, "error": error_msg},
153
+ ) from e
154
+
155
+
156
+ async def list_mcp_tools(
157
+ server_name: str,
158
+ internal_manager: "InternalToolRegistryManager | None" = Depends(get_internal_manager),
159
+ mcp_manager: "MCPClientManager | None" = Depends(get_mcp_manager),
160
+ ) -> dict[str, Any]:
161
+ """
162
+ List available tools from an MCP server.
163
+
164
+ Args:
165
+ server_name: Name of the MCP server (e.g., "supabase", "context7")
166
+ internal_manager: Internal tool registry manager (injected)
167
+ mcp_manager: External MCP client manager (injected)
168
+
169
+ Returns:
170
+ List of available tools with their descriptions
171
+ """
172
+ start_time = time.perf_counter()
173
+ _metrics.inc_counter("http_requests_total")
174
+
175
+ try:
176
+ # Check internal registries first (gobby-tasks, gobby-memory, etc.)
177
+ if internal_manager and internal_manager.is_internal(server_name):
178
+ registry = internal_manager.get_registry(server_name)
179
+ if registry:
180
+ tools = registry.list_tools()
181
+ response_time_ms = (time.perf_counter() - start_time) * 1000
182
+ _metrics.observe_histogram("list_mcp_tools", response_time_ms / 1000)
183
+ return {
184
+ "status": "success",
185
+ "tools": tools,
186
+ "tool_count": len(tools),
187
+ "response_time_ms": response_time_ms,
188
+ }
189
+ raise HTTPException(
190
+ status_code=404,
191
+ detail={
192
+ "success": False,
193
+ "error": f"Internal server '{server_name}' not found",
194
+ },
195
+ )
196
+
197
+ if mcp_manager is None:
198
+ raise HTTPException(
199
+ status_code=503, detail={"success": False, "error": "MCP manager not available"}
200
+ )
201
+
202
+ # Check if server is configured
203
+ if not mcp_manager.has_server(server_name):
204
+ raise HTTPException(
205
+ status_code=404,
206
+ detail={"success": False, "error": f"Unknown MCP server: '{server_name}'"},
207
+ )
208
+
209
+ # Use ensure_connected for lazy loading - connects on-demand if not connected
210
+ try:
211
+ session = await mcp_manager.ensure_connected(server_name)
212
+ except KeyError as e:
213
+ raise HTTPException(status_code=404, detail={"success": False, "error": str(e)}) from e
214
+ except Exception as e:
215
+ raise HTTPException(
216
+ status_code=503,
217
+ detail={
218
+ "success": False,
219
+ "error": f"MCP server '{server_name}' connection failed: {e}",
220
+ },
221
+ ) from e
222
+
223
+ # List tools using MCP SDK
224
+ try:
225
+ tools_result = await session.list_tools()
226
+ tools = []
227
+ for tool in tools_result.tools:
228
+ tool_dict: dict[str, Any] = {
229
+ "name": tool.name,
230
+ "description": tool.description if hasattr(tool, "description") else None,
231
+ }
232
+
233
+ # Handle inputSchema
234
+ if hasattr(tool, "inputSchema"):
235
+ schema = tool.inputSchema
236
+ if hasattr(schema, "model_dump"):
237
+ tool_dict["inputSchema"] = schema.model_dump()
238
+ elif isinstance(schema, dict):
239
+ tool_dict["inputSchema"] = schema
240
+ else:
241
+ tool_dict["inputSchema"] = None
242
+ else:
243
+ tool_dict["inputSchema"] = None
244
+
245
+ tools.append(tool_dict)
246
+
247
+ response_time_ms = (time.perf_counter() - start_time) * 1000
248
+
249
+ logger.debug(
250
+ f"Listed {len(tools)} tools from {server_name}",
251
+ extra={
252
+ "server": server_name,
253
+ "tool_count": len(tools),
254
+ "response_time_ms": response_time_ms,
255
+ },
256
+ )
257
+
258
+ return {
259
+ "status": "success",
260
+ "tools": tools,
261
+ "tool_count": len(tools),
262
+ "response_time_ms": response_time_ms,
263
+ }
264
+
265
+ except Exception as e:
266
+ logger.error(
267
+ f"Failed to list tools from {server_name}: {e}",
268
+ exc_info=True,
269
+ extra={"server": server_name},
270
+ )
271
+ raise HTTPException(
272
+ status_code=500,
273
+ detail={"success": False, "error": f"Failed to list tools: {e}"},
274
+ ) from e
275
+
276
+ except HTTPException:
277
+ raise
278
+ except Exception as e:
279
+ _metrics.inc_counter("http_requests_errors_total")
280
+ logger.error(f"MCP list tools error: {server_name}", exc_info=True)
281
+ raise HTTPException(status_code=500, detail={"success": False, "error": str(e)}) from e
282
+
283
+
284
+ async def get_tool_schema(
285
+ request: Request,
286
+ server: "HTTPServer" = Depends(get_server),
287
+ ) -> dict[str, Any]:
288
+ """
289
+ Get full schema for a specific tool.
290
+
291
+ Request body:
292
+ {
293
+ "server_name": "supabase",
294
+ "tool_name": "list_tables"
295
+ }
296
+
297
+ Returns:
298
+ Tool schema with inputSchema
299
+ """
300
+ start_time = time.perf_counter()
301
+ _metrics.inc_counter("http_requests_total")
302
+
303
+ try:
304
+ body = await request.json()
305
+ server_name = body.get("server_name")
306
+ tool_name = body.get("tool_name")
307
+
308
+ if not server_name or not tool_name:
309
+ raise HTTPException(
310
+ status_code=400,
311
+ detail={"success": False, "error": "Required fields: server_name, tool_name"},
312
+ )
313
+
314
+ # Check internal first
315
+ if server._internal_manager and server._internal_manager.is_internal(server_name):
316
+ registry = server._internal_manager.get_registry(server_name)
317
+ if registry:
318
+ schema = registry.get_schema(tool_name)
319
+ if schema:
320
+ response_time_ms = (time.perf_counter() - start_time) * 1000
321
+ return {
322
+ "name": tool_name,
323
+ "server": server_name,
324
+ "inputSchema": schema,
325
+ "response_time_ms": response_time_ms,
326
+ }
327
+ raise HTTPException(
328
+ status_code=404,
329
+ detail={
330
+ "success": False,
331
+ "error": f"Tool '{tool_name}' not found on server '{server_name}'",
332
+ },
333
+ )
334
+
335
+ if server.mcp_manager is None:
336
+ raise HTTPException(
337
+ status_code=503,
338
+ detail={"success": False, "error": "MCP manager not available"},
339
+ )
340
+
341
+ # Get from external MCP server
342
+ try:
343
+ schema = await server.mcp_manager.get_tool_input_schema(server_name, tool_name)
344
+ response_time_ms = (time.perf_counter() - start_time) * 1000
345
+
346
+ return {
347
+ "name": tool_name,
348
+ "server": server_name,
349
+ "inputSchema": schema,
350
+ "response_time_ms": response_time_ms,
351
+ }
352
+
353
+ except (KeyError, ValueError) as e:
354
+ # Tool or server not found - 404
355
+ raise HTTPException(status_code=404, detail={"success": False, "error": str(e)}) from e
356
+ except Exception as e:
357
+ # Connection, timeout, or internal errors - 500
358
+ logger.error(f"Failed to get tool schema {server_name}/{tool_name}: {e}", exc_info=True)
359
+ raise HTTPException(
360
+ status_code=500,
361
+ detail={"success": False, "error": f"Failed to get tool schema: {e}"},
362
+ ) from e
363
+
364
+ except HTTPException:
365
+ raise
366
+ except Exception as e:
367
+ _metrics.inc_counter("http_requests_errors_total")
368
+ logger.error(f"Get tool schema error: {e}", exc_info=True)
369
+ raise HTTPException(status_code=500, detail={"success": False, "error": str(e)}) from e
370
+
371
+
372
+ async def call_mcp_tool(
373
+ request: Request,
374
+ server: "HTTPServer" = Depends(get_server),
375
+ ) -> dict[str, Any]:
376
+ """
377
+ Call an MCP tool.
378
+
379
+ Request body:
380
+ {
381
+ "server_name": "supabase",
382
+ "tool_name": "list_tables",
383
+ "arguments": {}
384
+ }
385
+
386
+ Returns:
387
+ Tool execution result
388
+ """
389
+ start_time = time.perf_counter()
390
+ _metrics.inc_counter("http_requests_total")
391
+ _metrics.inc_counter("mcp_tool_calls_total")
392
+
393
+ try:
394
+ body = await request.json()
395
+ server_name = body.get("server_name")
396
+ tool_name = body.get("tool_name")
397
+ arguments = body.get("arguments", {})
398
+
399
+ if not server_name or not tool_name:
400
+ raise HTTPException(
401
+ status_code=400,
402
+ detail={"success": False, "error": "Required fields: server_name, tool_name"},
403
+ )
404
+
405
+ # Route through ToolProxyService for consistent error enrichment
406
+ if server.tool_proxy:
407
+ result = await server.tool_proxy.call_tool(server_name, tool_name, arguments)
408
+ response_time_ms = (time.perf_counter() - start_time) * 1000
409
+ return _process_tool_proxy_result(result, server_name, tool_name, response_time_ms)
410
+
411
+ # Fallback: no tool_proxy available, use direct registry calls
412
+ # Check internal first
413
+ if server._internal_manager and server._internal_manager.is_internal(server_name):
414
+ registry = server._internal_manager.get_registry(server_name)
415
+ if registry:
416
+ return await _call_internal_tool(
417
+ registry, server_name, tool_name, arguments, start_time
418
+ )
419
+
420
+ if server.mcp_manager is None:
421
+ raise HTTPException(
422
+ status_code=503,
423
+ detail={"success": False, "error": "MCP manager not available"},
424
+ )
425
+
426
+ # Call external MCP tool
427
+ try:
428
+ result = await server.mcp_manager.call_tool(server_name, tool_name, arguments)
429
+ response_time_ms = (time.perf_counter() - start_time) * 1000
430
+ _metrics.inc_counter("mcp_tool_calls_succeeded_total")
431
+
432
+ return {
433
+ "success": True,
434
+ "result": result,
435
+ "response_time_ms": response_time_ms,
436
+ }
437
+
438
+ except Exception as e:
439
+ _metrics.inc_counter("mcp_tool_calls_failed_total")
440
+ error_msg = str(e) or f"{type(e).__name__}: (no message)"
441
+ raise HTTPException(
442
+ status_code=500, detail={"success": False, "error": error_msg}
443
+ ) from e
444
+
445
+ except HTTPException:
446
+ raise
447
+ except Exception as e:
448
+ _metrics.inc_counter("mcp_tool_calls_failed_total")
449
+ error_msg = str(e) or f"{type(e).__name__}: (no message)"
450
+ logger.error(f"Call MCP tool error: {error_msg}", exc_info=True)
451
+ raise HTTPException(status_code=500, detail={"success": False, "error": error_msg}) from e
452
+
453
+
454
+ async def mcp_proxy(
455
+ server_name: str,
456
+ tool_name: str,
457
+ request: Request,
458
+ server: "HTTPServer" = Depends(get_server),
459
+ ) -> dict[str, Any]:
460
+ """
461
+ Unified MCP proxy endpoint for calling MCP server tools.
462
+
463
+ Args:
464
+ server_name: Name of the MCP server
465
+ tool_name: Name of the tool to call
466
+ request: FastAPI request with tool arguments in body
467
+
468
+ Returns:
469
+ Tool execution result
470
+ """
471
+ start_time = time.perf_counter()
472
+ _metrics.inc_counter("http_requests_total")
473
+ _metrics.inc_counter("mcp_tool_calls_total")
474
+
475
+ try:
476
+ # Parse request body as tool arguments
477
+ try:
478
+ args = await request.json()
479
+ except (json.JSONDecodeError, ValueError) as e:
480
+ raise HTTPException(
481
+ status_code=400,
482
+ detail={"success": False, "error": f"Invalid JSON in request body: {e}"},
483
+ ) from e
484
+
485
+ # Route through ToolProxyService for consistent error enrichment
486
+ if server.tool_proxy:
487
+ result = await server.tool_proxy.call_tool(server_name, tool_name, args)
488
+ response_time_ms = (time.perf_counter() - start_time) * 1000
489
+ return _process_tool_proxy_result(result, server_name, tool_name, response_time_ms)
490
+
491
+ # Fallback: no tool_proxy available, use direct registry calls
492
+ # Check internal registries first (gobby-tasks, gobby-memory, etc.)
493
+ if server._internal_manager and server._internal_manager.is_internal(server_name):
494
+ registry = server._internal_manager.get_registry(server_name)
495
+ if registry:
496
+ return await _call_internal_tool(registry, server_name, tool_name, args, start_time)
497
+ raise HTTPException(
498
+ status_code=404,
499
+ detail={
500
+ "success": False,
501
+ "error": f"Internal server '{server_name}' not found",
502
+ },
503
+ )
504
+
505
+ if server.mcp_manager is None:
506
+ raise HTTPException(
507
+ status_code=503,
508
+ detail={"success": False, "error": "MCP manager not available"},
509
+ )
510
+
511
+ # Call MCP tool
512
+ try:
513
+ result = await server.mcp_manager.call_tool(server_name, tool_name, args)
514
+
515
+ response_time_ms = (time.perf_counter() - start_time) * 1000
516
+
517
+ logger.debug(
518
+ f"MCP tool call successful: {server_name}.{tool_name}",
519
+ extra={
520
+ "server": server_name,
521
+ "tool": tool_name,
522
+ "response_time_ms": response_time_ms,
523
+ },
524
+ )
525
+
526
+ _metrics.inc_counter("mcp_tool_calls_succeeded_total")
527
+
528
+ return {
529
+ "success": True,
530
+ "result": result,
531
+ "response_time_ms": response_time_ms,
532
+ }
533
+
534
+ except ValueError as e:
535
+ _metrics.inc_counter("mcp_tool_calls_failed_total")
536
+ logger.warning(
537
+ f"MCP tool not found: {server_name}.{tool_name}",
538
+ extra={"server": server_name, "tool": tool_name, "error": str(e)},
539
+ )
540
+ raise HTTPException(status_code=404, detail={"success": False, "error": str(e)}) from e
541
+ except Exception as e:
542
+ _metrics.inc_counter("mcp_tool_calls_failed_total")
543
+ error_msg = str(e) or f"{type(e).__name__}: (no message)"
544
+ logger.error(
545
+ f"MCP tool call error: {server_name}.{tool_name}",
546
+ exc_info=True,
547
+ extra={"server": server_name, "tool": tool_name},
548
+ )
549
+ raise HTTPException(
550
+ status_code=500, detail={"success": False, "error": error_msg}
551
+ ) from e
552
+
553
+ except HTTPException:
554
+ raise
555
+ except Exception as e:
556
+ _metrics.inc_counter("mcp_tool_calls_failed_total")
557
+ error_msg = str(e) or f"{type(e).__name__}: (no message)"
558
+ logger.error(f"MCP proxy error: {server_name}.{tool_name}", exc_info=True)
559
+ raise HTTPException(status_code=500, detail={"success": False, "error": error_msg}) from e
560
+
561
+
562
+ __all__ = [
563
+ "list_mcp_tools",
564
+ "get_tool_schema",
565
+ "call_mcp_tool",
566
+ "mcp_proxy",
567
+ "_process_tool_proxy_result",
568
+ ]