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.
- gobby/__init__.py +1 -1
- gobby/adapters/__init__.py +2 -1
- gobby/adapters/claude_code.py +13 -4
- 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/runner.py +8 -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/__init__.py +6 -0
- gobby/cli/clones.py +419 -0
- gobby/cli/conductor.py +266 -0
- gobby/cli/install.py +4 -4
- gobby/cli/installers/antigravity.py +3 -9
- gobby/cli/installers/claude.py +15 -9
- gobby/cli/installers/codex.py +2 -8
- gobby/cli/installers/gemini.py +8 -8
- gobby/cli/installers/shared.py +175 -13
- gobby/cli/sessions.py +1 -1
- gobby/cli/skills.py +858 -0
- gobby/cli/tasks/ai.py +0 -440
- gobby/cli/tasks/crud.py +44 -6
- gobby/cli/tasks/main.py +0 -4
- gobby/cli/tui.py +2 -2
- gobby/cli/utils.py +12 -5
- gobby/clones/__init__.py +13 -0
- gobby/clones/git.py +547 -0
- gobby/conductor/__init__.py +16 -0
- gobby/conductor/alerts.py +135 -0
- gobby/conductor/loop.py +164 -0
- gobby/conductor/monitors/__init__.py +11 -0
- gobby/conductor/monitors/agents.py +116 -0
- gobby/conductor/monitors/tasks.py +155 -0
- gobby/conductor/pricing.py +234 -0
- gobby/conductor/token_tracker.py +160 -0
- gobby/config/__init__.py +12 -97
- gobby/config/app.py +69 -91
- gobby/config/extensions.py +2 -2
- gobby/config/features.py +7 -130
- gobby/config/search.py +110 -0
- gobby/config/servers.py +1 -1
- gobby/config/skills.py +43 -0
- gobby/config/tasks.py +9 -41
- gobby/hooks/__init__.py +0 -13
- gobby/hooks/event_handlers.py +188 -2
- gobby/hooks/hook_manager.py +50 -4
- gobby/hooks/plugins.py +1 -1
- gobby/hooks/skill_manager.py +130 -0
- gobby/hooks/webhooks.py +1 -1
- gobby/install/claude/hooks/hook_dispatcher.py +4 -4
- gobby/install/codex/hooks/hook_dispatcher.py +1 -1
- gobby/install/gemini/hooks/hook_dispatcher.py +87 -12
- gobby/llm/claude.py +22 -34
- gobby/llm/claude_executor.py +46 -256
- gobby/llm/codex_executor.py +59 -291
- gobby/llm/executor.py +21 -0
- gobby/llm/gemini.py +134 -110
- gobby/llm/litellm_executor.py +143 -6
- gobby/llm/resolver.py +98 -35
- gobby/mcp_proxy/importer.py +62 -4
- gobby/mcp_proxy/instructions.py +56 -0
- gobby/mcp_proxy/models.py +15 -0
- gobby/mcp_proxy/registries.py +68 -8
- gobby/mcp_proxy/server.py +33 -3
- gobby/mcp_proxy/services/recommendation.py +43 -11
- gobby/mcp_proxy/services/tool_proxy.py +81 -1
- gobby/mcp_proxy/stdio.py +2 -1
- gobby/mcp_proxy/tools/__init__.py +0 -2
- gobby/mcp_proxy/tools/agent_messaging.py +317 -0
- gobby/mcp_proxy/tools/agents.py +31 -731
- gobby/mcp_proxy/tools/clones.py +518 -0
- gobby/mcp_proxy/tools/memory.py +3 -26
- gobby/mcp_proxy/tools/metrics.py +65 -1
- gobby/mcp_proxy/tools/orchestration/__init__.py +3 -0
- gobby/mcp_proxy/tools/orchestration/cleanup.py +151 -0
- gobby/mcp_proxy/tools/orchestration/wait.py +467 -0
- 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 +616 -0
- gobby/mcp_proxy/tools/spawn_agent.py +417 -0
- gobby/mcp_proxy/tools/task_orchestration.py +7 -0
- gobby/mcp_proxy/tools/task_readiness.py +14 -0
- gobby/mcp_proxy/tools/task_sync.py +1 -1
- gobby/mcp_proxy/tools/tasks/_context.py +0 -20
- gobby/mcp_proxy/tools/tasks/_crud.py +91 -4
- gobby/mcp_proxy/tools/tasks/_expansion.py +348 -0
- gobby/mcp_proxy/tools/tasks/_factory.py +6 -16
- gobby/mcp_proxy/tools/tasks/_lifecycle.py +110 -45
- gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +18 -29
- gobby/mcp_proxy/tools/workflows.py +1 -1
- gobby/mcp_proxy/tools/worktrees.py +0 -338
- gobby/memory/backends/__init__.py +6 -1
- gobby/memory/backends/mem0.py +6 -1
- gobby/memory/extractor.py +477 -0
- gobby/memory/ingestion/__init__.py +5 -0
- gobby/memory/ingestion/multimodal.py +221 -0
- gobby/memory/manager.py +73 -285
- 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/runner.py +37 -16
- gobby/search/__init__.py +48 -6
- gobby/search/backends/__init__.py +159 -0
- gobby/search/backends/embedding.py +225 -0
- gobby/search/embeddings.py +238 -0
- gobby/search/models.py +148 -0
- gobby/search/unified.py +496 -0
- gobby/servers/http.py +24 -12
- gobby/servers/routes/admin.py +294 -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 -1317
- gobby/servers/websocket.py +2 -2
- gobby/sessions/analyzer.py +2 -0
- gobby/sessions/lifecycle.py +1 -1
- gobby/sessions/processor.py +10 -0
- gobby/sessions/transcripts/base.py +2 -0
- gobby/sessions/transcripts/claude.py +79 -10
- gobby/skills/__init__.py +91 -0
- gobby/skills/loader.py +685 -0
- gobby/skills/manager.py +384 -0
- gobby/skills/parser.py +286 -0
- gobby/skills/search.py +463 -0
- gobby/skills/sync.py +119 -0
- gobby/skills/updater.py +385 -0
- gobby/skills/validator.py +368 -0
- gobby/storage/clones.py +378 -0
- gobby/storage/database.py +1 -1
- gobby/storage/memories.py +43 -13
- gobby/storage/migrations.py +162 -201
- gobby/storage/sessions.py +116 -7
- gobby/storage/skills.py +782 -0
- gobby/storage/tasks/_crud.py +4 -4
- gobby/storage/tasks/_lifecycle.py +57 -7
- gobby/storage/tasks/_manager.py +14 -5
- gobby/storage/tasks/_models.py +8 -3
- gobby/sync/memories.py +40 -5
- gobby/sync/tasks.py +83 -6
- gobby/tasks/__init__.py +1 -2
- gobby/tasks/external_validator.py +1 -1
- gobby/tasks/validation.py +46 -35
- gobby/tools/summarizer.py +91 -10
- gobby/tui/api_client.py +4 -7
- gobby/tui/app.py +5 -3
- gobby/tui/screens/orchestrator.py +1 -2
- gobby/tui/screens/tasks.py +2 -4
- gobby/tui/ws_client.py +1 -1
- gobby/utils/daemon_client.py +2 -2
- gobby/utils/project_context.py +2 -3
- gobby/utils/status.py +13 -0
- gobby/workflows/actions.py +221 -1135
- gobby/workflows/artifact_actions.py +31 -0
- gobby/workflows/autonomous_actions.py +11 -0
- gobby/workflows/context_actions.py +93 -1
- gobby/workflows/detection_helpers.py +115 -31
- 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/{task_enforcement_actions.py → enforcement/task_policy.py} +29 -388
- gobby/workflows/engine.py +13 -2
- gobby/workflows/git_utils.py +106 -0
- gobby/workflows/lifecycle_evaluator.py +29 -1
- gobby/workflows/llm_actions.py +30 -0
- gobby/workflows/loader.py +19 -6
- gobby/workflows/mcp_actions.py +20 -1
- gobby/workflows/memory_actions.py +154 -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 +111 -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.5.dist-info → gobby-0.2.7.dist-info}/METADATA +87 -21
- {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/RECORD +201 -172
- {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/WHEEL +1 -1
- gobby/adapters/codex.py +0 -1292
- 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/install/codex/prompts/forget.md +0 -7
- gobby/install/codex/prompts/memories.md +0 -7
- gobby/install/codex/prompts/recall.md +0 -7
- gobby/install/codex/prompts/remember.md +0 -13
- gobby/llm/gemini_executor.py +0 -339
- gobby/mcp_proxy/tools/session_messages.py +0 -1056
- gobby/mcp_proxy/tools/task_expansion.py +0 -591
- 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/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/tasks/context.py +0 -747
- gobby/tasks/criteria.py +0 -342
- gobby/tasks/expansion.py +0 -626
- gobby/tasks/prompts/expand.py +0 -327
- gobby/tasks/research.py +0 -421
- gobby/tasks/tdd.py +0 -352
- {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/entry_points.txt +0 -0
- {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/licenses/LICENSE.md +0 -0
- {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
|
-
|
|
5
|
-
|
|
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
|
|
9
|
-
import logging
|
|
10
|
-
import time
|
|
11
|
-
from typing import TYPE_CHECKING, Any
|
|
11
|
+
from fastapi import APIRouter
|
|
12
12
|
|
|
13
|
-
from
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
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
|