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.
- gobby/__init__.py +1 -1
- gobby/adapters/__init__.py +2 -1
- gobby/adapters/codex_impl/__init__.py +28 -0
- gobby/adapters/codex_impl/adapter.py +722 -0
- gobby/adapters/codex_impl/client.py +679 -0
- gobby/adapters/codex_impl/protocol.py +20 -0
- gobby/adapters/codex_impl/types.py +68 -0
- gobby/agents/definitions.py +11 -1
- gobby/agents/isolation.py +395 -0
- gobby/agents/sandbox.py +261 -0
- gobby/agents/spawn.py +42 -287
- gobby/agents/spawn_executor.py +385 -0
- gobby/agents/spawners/__init__.py +24 -0
- gobby/agents/spawners/command_builder.py +189 -0
- gobby/agents/spawners/embedded.py +21 -2
- gobby/agents/spawners/headless.py +21 -2
- gobby/agents/spawners/prompt_manager.py +125 -0
- gobby/cli/install.py +4 -4
- gobby/cli/installers/claude.py +6 -0
- gobby/cli/installers/gemini.py +6 -0
- gobby/cli/installers/shared.py +103 -4
- gobby/cli/sessions.py +1 -1
- gobby/cli/utils.py +9 -2
- gobby/config/__init__.py +12 -97
- gobby/config/app.py +10 -94
- gobby/config/extensions.py +2 -2
- gobby/config/features.py +7 -130
- gobby/config/tasks.py +4 -28
- gobby/hooks/__init__.py +0 -13
- gobby/hooks/event_handlers.py +45 -2
- gobby/hooks/hook_manager.py +2 -2
- gobby/hooks/plugins.py +1 -1
- gobby/hooks/webhooks.py +1 -1
- gobby/llm/resolver.py +3 -2
- gobby/mcp_proxy/importer.py +62 -4
- gobby/mcp_proxy/instructions.py +2 -0
- gobby/mcp_proxy/registries.py +1 -4
- gobby/mcp_proxy/services/recommendation.py +43 -11
- gobby/mcp_proxy/tools/agents.py +31 -731
- gobby/mcp_proxy/tools/clones.py +0 -385
- gobby/mcp_proxy/tools/memory.py +2 -2
- gobby/mcp_proxy/tools/sessions/__init__.py +14 -0
- gobby/mcp_proxy/tools/sessions/_commits.py +232 -0
- gobby/mcp_proxy/tools/sessions/_crud.py +253 -0
- gobby/mcp_proxy/tools/sessions/_factory.py +63 -0
- gobby/mcp_proxy/tools/sessions/_handoff.py +499 -0
- gobby/mcp_proxy/tools/sessions/_messages.py +138 -0
- gobby/mcp_proxy/tools/skills/__init__.py +14 -29
- gobby/mcp_proxy/tools/spawn_agent.py +417 -0
- gobby/mcp_proxy/tools/tasks/_lifecycle.py +52 -18
- gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +1 -1
- gobby/mcp_proxy/tools/worktrees.py +0 -343
- gobby/memory/ingestion/__init__.py +5 -0
- gobby/memory/ingestion/multimodal.py +221 -0
- gobby/memory/manager.py +62 -283
- gobby/memory/search/__init__.py +10 -0
- gobby/memory/search/coordinator.py +248 -0
- gobby/memory/services/__init__.py +5 -0
- gobby/memory/services/crossref.py +142 -0
- gobby/prompts/loader.py +5 -2
- gobby/servers/http.py +1 -4
- gobby/servers/routes/admin.py +14 -0
- gobby/servers/routes/mcp/endpoints/__init__.py +61 -0
- gobby/servers/routes/mcp/endpoints/discovery.py +405 -0
- gobby/servers/routes/mcp/endpoints/execution.py +568 -0
- gobby/servers/routes/mcp/endpoints/registry.py +378 -0
- gobby/servers/routes/mcp/endpoints/server.py +304 -0
- gobby/servers/routes/mcp/hooks.py +1 -1
- gobby/servers/routes/mcp/tools.py +48 -1506
- gobby/sessions/lifecycle.py +1 -1
- gobby/sessions/processor.py +10 -0
- gobby/sessions/transcripts/base.py +1 -0
- gobby/sessions/transcripts/claude.py +15 -5
- gobby/skills/parser.py +30 -2
- gobby/storage/migrations.py +159 -372
- gobby/storage/sessions.py +43 -7
- gobby/storage/skills.py +37 -4
- gobby/storage/tasks/_lifecycle.py +18 -3
- gobby/sync/memories.py +1 -1
- gobby/tasks/external_validator.py +1 -1
- gobby/tasks/validation.py +22 -20
- gobby/tools/summarizer.py +91 -10
- gobby/utils/project_context.py +2 -3
- gobby/utils/status.py +13 -0
- gobby/workflows/actions.py +221 -1217
- gobby/workflows/artifact_actions.py +31 -0
- gobby/workflows/autonomous_actions.py +11 -0
- gobby/workflows/context_actions.py +50 -1
- gobby/workflows/enforcement/__init__.py +47 -0
- gobby/workflows/enforcement/blocking.py +269 -0
- gobby/workflows/enforcement/commit_policy.py +283 -0
- gobby/workflows/enforcement/handlers.py +269 -0
- gobby/workflows/enforcement/task_policy.py +542 -0
- gobby/workflows/git_utils.py +106 -0
- gobby/workflows/llm_actions.py +30 -0
- gobby/workflows/mcp_actions.py +20 -1
- gobby/workflows/memory_actions.py +80 -0
- gobby/workflows/safe_evaluator.py +183 -0
- gobby/workflows/session_actions.py +44 -0
- gobby/workflows/state_actions.py +60 -1
- gobby/workflows/stop_signal_actions.py +55 -0
- gobby/workflows/summary_actions.py +94 -1
- gobby/workflows/task_sync_actions.py +347 -0
- gobby/workflows/todo_actions.py +34 -1
- gobby/workflows/webhook_actions.py +185 -0
- {gobby-0.2.6.dist-info → gobby-0.2.7.dist-info}/METADATA +6 -1
- {gobby-0.2.6.dist-info → gobby-0.2.7.dist-info}/RECORD +111 -111
- {gobby-0.2.6.dist-info → gobby-0.2.7.dist-info}/WHEEL +1 -1
- gobby/adapters/codex.py +0 -1332
- gobby/install/claude/commands/gobby/bug.md +0 -51
- gobby/install/claude/commands/gobby/chore.md +0 -51
- gobby/install/claude/commands/gobby/epic.md +0 -52
- gobby/install/claude/commands/gobby/eval.md +0 -235
- gobby/install/claude/commands/gobby/feat.md +0 -49
- gobby/install/claude/commands/gobby/nit.md +0 -52
- gobby/install/claude/commands/gobby/ref.md +0 -52
- gobby/mcp_proxy/tools/session_messages.py +0 -1055
- gobby/prompts/defaults/expansion/system.md +0 -119
- gobby/prompts/defaults/expansion/user.md +0 -48
- gobby/prompts/defaults/external_validation/agent.md +0 -72
- gobby/prompts/defaults/external_validation/external.md +0 -63
- gobby/prompts/defaults/external_validation/spawn.md +0 -83
- gobby/prompts/defaults/external_validation/system.md +0 -6
- gobby/prompts/defaults/features/import_mcp.md +0 -22
- gobby/prompts/defaults/features/import_mcp_github.md +0 -17
- gobby/prompts/defaults/features/import_mcp_search.md +0 -16
- gobby/prompts/defaults/features/recommend_tools.md +0 -32
- gobby/prompts/defaults/features/recommend_tools_hybrid.md +0 -35
- gobby/prompts/defaults/features/recommend_tools_llm.md +0 -30
- gobby/prompts/defaults/features/server_description.md +0 -20
- gobby/prompts/defaults/features/server_description_system.md +0 -6
- gobby/prompts/defaults/features/task_description.md +0 -31
- gobby/prompts/defaults/features/task_description_system.md +0 -6
- gobby/prompts/defaults/features/tool_summary.md +0 -17
- gobby/prompts/defaults/features/tool_summary_system.md +0 -6
- gobby/prompts/defaults/handoff/compact.md +0 -63
- gobby/prompts/defaults/handoff/session_end.md +0 -57
- gobby/prompts/defaults/memory/extract.md +0 -61
- gobby/prompts/defaults/research/step.md +0 -58
- gobby/prompts/defaults/validation/criteria.md +0 -47
- gobby/prompts/defaults/validation/validate.md +0 -38
- gobby/storage/migrations_legacy.py +0 -1359
- gobby/workflows/task_enforcement_actions.py +0 -1343
- {gobby-0.2.6.dist-info → gobby-0.2.7.dist-info}/entry_points.txt +0 -0
- {gobby-0.2.6.dist-info → gobby-0.2.7.dist-info}/licenses/LICENSE.md +0 -0
- {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
|
+
]
|