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
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Registry endpoints for MCP tool embedding, status, and refresh.
|
|
3
|
+
|
|
4
|
+
Extracted from tools.py as part of Phase 2 Strangler Fig decomposition.
|
|
5
|
+
These endpoints handle tool registry operations like embedding, status, and refresh.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
import time
|
|
10
|
+
from typing import TYPE_CHECKING, Any
|
|
11
|
+
|
|
12
|
+
from fastapi import Depends, HTTPException, Request
|
|
13
|
+
|
|
14
|
+
from gobby.servers.routes.dependencies import get_server
|
|
15
|
+
from gobby.utils.metrics import get_metrics_collector
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from gobby.servers.http import HTTPServer
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
# Module-level metrics collector (shared across all requests)
|
|
23
|
+
_metrics = get_metrics_collector()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
async def embed_mcp_tools(
|
|
27
|
+
request: Request,
|
|
28
|
+
server: "HTTPServer" = Depends(get_server),
|
|
29
|
+
) -> dict[str, Any]:
|
|
30
|
+
"""
|
|
31
|
+
Generate embeddings for all tools in a project.
|
|
32
|
+
|
|
33
|
+
Request body:
|
|
34
|
+
{
|
|
35
|
+
"cwd": "/path/to/project",
|
|
36
|
+
"force": false
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
Embedding generation stats
|
|
41
|
+
"""
|
|
42
|
+
start_time = time.perf_counter()
|
|
43
|
+
_metrics.inc_counter("http_requests_total")
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
body = await request.json()
|
|
47
|
+
cwd = body.get("cwd")
|
|
48
|
+
force = body.get("force", False)
|
|
49
|
+
|
|
50
|
+
# Resolve project_id from cwd
|
|
51
|
+
try:
|
|
52
|
+
project_id = server._resolve_project_id(None, cwd)
|
|
53
|
+
except ValueError as e:
|
|
54
|
+
return {
|
|
55
|
+
"success": False,
|
|
56
|
+
"error": str(e),
|
|
57
|
+
"response_time_ms": (time.perf_counter() - start_time) * 1000,
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
# Use semantic search to embed all tools
|
|
61
|
+
if server._tools_handler and server._tools_handler._semantic_search:
|
|
62
|
+
try:
|
|
63
|
+
stats = await server._tools_handler._semantic_search.embed_all_tools(
|
|
64
|
+
project_id=project_id,
|
|
65
|
+
mcp_manager=server._mcp_db_manager,
|
|
66
|
+
force=force,
|
|
67
|
+
)
|
|
68
|
+
response_time_ms = (time.perf_counter() - start_time) * 1000
|
|
69
|
+
return {
|
|
70
|
+
"success": True,
|
|
71
|
+
"stats": stats,
|
|
72
|
+
"response_time_ms": response_time_ms,
|
|
73
|
+
}
|
|
74
|
+
except Exception as e:
|
|
75
|
+
logger.error(f"Embedding generation failed: {e}")
|
|
76
|
+
return {
|
|
77
|
+
"success": False,
|
|
78
|
+
"error": str(e),
|
|
79
|
+
"response_time_ms": (time.perf_counter() - start_time) * 1000,
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
"success": False,
|
|
84
|
+
"error": "Semantic search not configured",
|
|
85
|
+
"response_time_ms": (time.perf_counter() - start_time) * 1000,
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
except HTTPException:
|
|
89
|
+
raise
|
|
90
|
+
except Exception as e:
|
|
91
|
+
_metrics.inc_counter("http_requests_errors_total")
|
|
92
|
+
logger.error(f"Embed tools error: {e}", exc_info=True)
|
|
93
|
+
raise HTTPException(status_code=500, detail={"success": False, "error": str(e)}) from e
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
async def get_mcp_status(
|
|
97
|
+
server: "HTTPServer" = Depends(get_server),
|
|
98
|
+
) -> dict[str, Any]:
|
|
99
|
+
"""
|
|
100
|
+
Get MCP proxy status and health.
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
Status summary with server counts and health info
|
|
104
|
+
"""
|
|
105
|
+
start_time = time.perf_counter()
|
|
106
|
+
_metrics.inc_counter("http_requests_total")
|
|
107
|
+
|
|
108
|
+
try:
|
|
109
|
+
total_servers = 0
|
|
110
|
+
connected_servers = 0
|
|
111
|
+
cached_tools = 0
|
|
112
|
+
server_health: dict[str, dict[str, Any]] = {}
|
|
113
|
+
|
|
114
|
+
# Count internal servers
|
|
115
|
+
if server._internal_manager:
|
|
116
|
+
for registry in server._internal_manager.get_all_registries():
|
|
117
|
+
total_servers += 1
|
|
118
|
+
connected_servers += 1
|
|
119
|
+
cached_tools += len(registry.list_tools())
|
|
120
|
+
server_health[registry.name] = {
|
|
121
|
+
"state": "connected",
|
|
122
|
+
"health": "healthy",
|
|
123
|
+
"failures": 0,
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
# Count external servers
|
|
127
|
+
if server.mcp_manager:
|
|
128
|
+
for config in server.mcp_manager.server_configs:
|
|
129
|
+
total_servers += 1
|
|
130
|
+
health = server.mcp_manager.health.get(config.name)
|
|
131
|
+
is_connected = config.name in server.mcp_manager.connections
|
|
132
|
+
if is_connected:
|
|
133
|
+
connected_servers += 1
|
|
134
|
+
|
|
135
|
+
server_health[config.name] = {
|
|
136
|
+
"state": health.state.value if health else "unknown",
|
|
137
|
+
"health": health.health.value if health else "unknown",
|
|
138
|
+
"failures": health.consecutive_failures if health else 0,
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
response_time_ms = (time.perf_counter() - start_time) * 1000
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
"total_servers": total_servers,
|
|
145
|
+
"connected_servers": connected_servers,
|
|
146
|
+
"cached_tools": cached_tools,
|
|
147
|
+
"server_health": server_health,
|
|
148
|
+
"response_time_ms": response_time_ms,
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
except Exception as e:
|
|
152
|
+
_metrics.inc_counter("http_requests_errors_total")
|
|
153
|
+
logger.error(f"Get MCP status error: {e}", exc_info=True)
|
|
154
|
+
raise HTTPException(status_code=500, detail={"success": False, "error": str(e)}) from e
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
async def refresh_mcp_tools(
|
|
158
|
+
request: Request,
|
|
159
|
+
server: "HTTPServer" = Depends(get_server),
|
|
160
|
+
) -> dict[str, Any]:
|
|
161
|
+
"""
|
|
162
|
+
Refresh MCP tools - detect schema changes and re-index as needed.
|
|
163
|
+
|
|
164
|
+
Request body:
|
|
165
|
+
{
|
|
166
|
+
"cwd": "/path/to/project",
|
|
167
|
+
"force": false,
|
|
168
|
+
"server": "optional-server-filter"
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
Refresh stats with new/changed/unchanged tool counts
|
|
173
|
+
"""
|
|
174
|
+
start_time = time.perf_counter()
|
|
175
|
+
_metrics.inc_counter("http_requests_total")
|
|
176
|
+
|
|
177
|
+
try:
|
|
178
|
+
body = await request.json()
|
|
179
|
+
cwd = body.get("cwd")
|
|
180
|
+
force = body.get("force", False)
|
|
181
|
+
server_filter = body.get("server")
|
|
182
|
+
|
|
183
|
+
# Resolve project_id from cwd
|
|
184
|
+
try:
|
|
185
|
+
project_id = server._resolve_project_id(None, cwd)
|
|
186
|
+
except ValueError as e:
|
|
187
|
+
return {
|
|
188
|
+
"success": False,
|
|
189
|
+
"error": str(e),
|
|
190
|
+
"response_time_ms": (time.perf_counter() - start_time) * 1000,
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
# Need schema hash manager and semantic search
|
|
194
|
+
if not server._mcp_db_manager:
|
|
195
|
+
return {
|
|
196
|
+
"success": False,
|
|
197
|
+
"error": "MCP database manager not configured",
|
|
198
|
+
"response_time_ms": (time.perf_counter() - start_time) * 1000,
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
from gobby.mcp_proxy.schema_hash import SchemaHashManager, compute_schema_hash
|
|
202
|
+
|
|
203
|
+
schema_hash_manager = SchemaHashManager(db=server._mcp_db_manager.db)
|
|
204
|
+
semantic_search = (
|
|
205
|
+
getattr(server._tools_handler, "_semantic_search", None)
|
|
206
|
+
if server._tools_handler
|
|
207
|
+
else None
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
stats: dict[str, Any] = {
|
|
211
|
+
"servers_processed": 0,
|
|
212
|
+
"tools_new": 0,
|
|
213
|
+
"tools_changed": 0,
|
|
214
|
+
"tools_unchanged": 0,
|
|
215
|
+
"tools_removed": 0,
|
|
216
|
+
"embeddings_generated": 0,
|
|
217
|
+
"by_server": {},
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
# Collect servers to process
|
|
221
|
+
servers_to_process: list[str] = []
|
|
222
|
+
|
|
223
|
+
# Internal servers
|
|
224
|
+
if server._internal_manager:
|
|
225
|
+
for registry in server._internal_manager.get_all_registries():
|
|
226
|
+
if server_filter is None or registry.name == server_filter:
|
|
227
|
+
servers_to_process.append(registry.name)
|
|
228
|
+
|
|
229
|
+
# External MCP servers
|
|
230
|
+
if server.mcp_manager:
|
|
231
|
+
for config in server.mcp_manager.server_configs:
|
|
232
|
+
if config.enabled:
|
|
233
|
+
if server_filter is None or config.name == server_filter:
|
|
234
|
+
servers_to_process.append(config.name)
|
|
235
|
+
|
|
236
|
+
# Process each server
|
|
237
|
+
for server_name in servers_to_process:
|
|
238
|
+
try:
|
|
239
|
+
tools: list[dict[str, Any]] = []
|
|
240
|
+
|
|
241
|
+
# Get tools from internal or external server
|
|
242
|
+
if server._internal_manager and server._internal_manager.is_internal(server_name):
|
|
243
|
+
internal_registry = server._internal_manager.get_registry(server_name)
|
|
244
|
+
if internal_registry:
|
|
245
|
+
for t in internal_registry.list_tools():
|
|
246
|
+
tool_name = t.get("name", "")
|
|
247
|
+
tools.append(
|
|
248
|
+
{
|
|
249
|
+
"name": tool_name,
|
|
250
|
+
"description": t.get("description"),
|
|
251
|
+
"inputSchema": internal_registry.get_schema(tool_name),
|
|
252
|
+
}
|
|
253
|
+
)
|
|
254
|
+
elif server.mcp_manager:
|
|
255
|
+
try:
|
|
256
|
+
session = await server.mcp_manager.ensure_connected(server_name)
|
|
257
|
+
tools_result = await session.list_tools()
|
|
258
|
+
for t in tools_result.tools:
|
|
259
|
+
schema = None
|
|
260
|
+
if hasattr(t, "inputSchema"):
|
|
261
|
+
if hasattr(t.inputSchema, "model_dump"):
|
|
262
|
+
schema = t.inputSchema.model_dump()
|
|
263
|
+
elif isinstance(t.inputSchema, dict):
|
|
264
|
+
schema = t.inputSchema
|
|
265
|
+
tools.append(
|
|
266
|
+
{
|
|
267
|
+
"name": getattr(t, "name", ""),
|
|
268
|
+
"description": getattr(t, "description", ""),
|
|
269
|
+
"inputSchema": schema,
|
|
270
|
+
}
|
|
271
|
+
)
|
|
272
|
+
except Exception as e:
|
|
273
|
+
logger.warning(f"Failed to connect to {server_name}: {e}")
|
|
274
|
+
stats["by_server"][server_name] = {"error": str(e)}
|
|
275
|
+
continue
|
|
276
|
+
|
|
277
|
+
# Check for schema changes
|
|
278
|
+
if force:
|
|
279
|
+
# Force mode: treat all as new
|
|
280
|
+
changes = {
|
|
281
|
+
"new": [t["name"] for t in tools],
|
|
282
|
+
"changed": [],
|
|
283
|
+
"unchanged": [],
|
|
284
|
+
}
|
|
285
|
+
else:
|
|
286
|
+
changes = schema_hash_manager.check_tools_for_changes(
|
|
287
|
+
server_name=server_name,
|
|
288
|
+
project_id=project_id,
|
|
289
|
+
tools=tools,
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
server_stats = {
|
|
293
|
+
"new": len(changes["new"]),
|
|
294
|
+
"changed": len(changes["changed"]),
|
|
295
|
+
"unchanged": len(changes["unchanged"]),
|
|
296
|
+
"removed": 0,
|
|
297
|
+
"embeddings": 0,
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
# Update schema hashes for new/changed tools
|
|
301
|
+
tools_to_embed = []
|
|
302
|
+
for tool in tools:
|
|
303
|
+
tool_name = tool["name"]
|
|
304
|
+
if tool_name in changes["new"] or tool_name in changes["changed"]:
|
|
305
|
+
schema = tool.get("inputSchema")
|
|
306
|
+
schema_hash = compute_schema_hash(schema)
|
|
307
|
+
schema_hash_manager.store_hash(
|
|
308
|
+
server_name=server_name,
|
|
309
|
+
tool_name=tool_name,
|
|
310
|
+
project_id=project_id,
|
|
311
|
+
schema_hash=schema_hash,
|
|
312
|
+
)
|
|
313
|
+
tools_to_embed.append(tool)
|
|
314
|
+
else:
|
|
315
|
+
# Just update verification time for unchanged
|
|
316
|
+
schema_hash_manager.update_verification_time(
|
|
317
|
+
server_name=server_name,
|
|
318
|
+
tool_name=tool_name,
|
|
319
|
+
project_id=project_id,
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
# Clean up stale hashes
|
|
323
|
+
valid_tool_names = [t["name"] for t in tools]
|
|
324
|
+
removed = schema_hash_manager.cleanup_stale_hashes(
|
|
325
|
+
server_name=server_name,
|
|
326
|
+
project_id=project_id,
|
|
327
|
+
valid_tool_names=valid_tool_names,
|
|
328
|
+
)
|
|
329
|
+
server_stats["removed"] = removed
|
|
330
|
+
|
|
331
|
+
# Generate embeddings for new/changed tools
|
|
332
|
+
if semantic_search and tools_to_embed:
|
|
333
|
+
for tool in tools_to_embed:
|
|
334
|
+
try:
|
|
335
|
+
await semantic_search.embed_tool(
|
|
336
|
+
server_name=server_name,
|
|
337
|
+
tool_name=tool["name"],
|
|
338
|
+
description=tool.get("description", ""),
|
|
339
|
+
input_schema=tool.get("inputSchema"),
|
|
340
|
+
project_id=project_id,
|
|
341
|
+
)
|
|
342
|
+
server_stats["embeddings"] += 1
|
|
343
|
+
except Exception as e:
|
|
344
|
+
logger.warning(f"Failed to embed {server_name}/{tool['name']}: {e}")
|
|
345
|
+
|
|
346
|
+
stats["by_server"][server_name] = server_stats
|
|
347
|
+
stats["servers_processed"] += 1
|
|
348
|
+
stats["tools_new"] += server_stats["new"]
|
|
349
|
+
stats["tools_changed"] += server_stats["changed"]
|
|
350
|
+
stats["tools_unchanged"] += server_stats["unchanged"]
|
|
351
|
+
stats["tools_removed"] += server_stats["removed"]
|
|
352
|
+
stats["embeddings_generated"] += server_stats["embeddings"]
|
|
353
|
+
|
|
354
|
+
except Exception as e:
|
|
355
|
+
logger.error(f"Error processing server {server_name}: {e}")
|
|
356
|
+
stats["by_server"][server_name] = {"error": str(e)}
|
|
357
|
+
|
|
358
|
+
response_time_ms = (time.perf_counter() - start_time) * 1000
|
|
359
|
+
return {
|
|
360
|
+
"success": True,
|
|
361
|
+
"force": force,
|
|
362
|
+
"stats": stats,
|
|
363
|
+
"response_time_ms": response_time_ms,
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
except HTTPException:
|
|
367
|
+
raise
|
|
368
|
+
except Exception as e:
|
|
369
|
+
_metrics.inc_counter("http_requests_errors_total")
|
|
370
|
+
logger.error(f"Refresh tools error: {e}", exc_info=True)
|
|
371
|
+
raise HTTPException(status_code=500, detail={"success": False, "error": str(e)}) from e
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
__all__ = [
|
|
375
|
+
"embed_mcp_tools",
|
|
376
|
+
"get_mcp_status",
|
|
377
|
+
"refresh_mcp_tools",
|
|
378
|
+
]
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Server management endpoints for MCP server lifecycle.
|
|
3
|
+
|
|
4
|
+
Extracted from tools.py as part of Phase 2 Strangler Fig decomposition.
|
|
5
|
+
These endpoints handle server listing, addition, import, and removal.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
import time
|
|
10
|
+
from typing import TYPE_CHECKING, Any
|
|
11
|
+
|
|
12
|
+
from fastapi import Depends, HTTPException, Request
|
|
13
|
+
|
|
14
|
+
from gobby.servers.routes.dependencies import get_internal_manager, get_mcp_manager, get_server
|
|
15
|
+
from gobby.utils.metrics import get_metrics_collector
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from gobby.mcp_proxy.manager import MCPClientManager
|
|
19
|
+
from gobby.mcp_proxy.registry_manager import InternalToolRegistryManager
|
|
20
|
+
from gobby.servers.http import HTTPServer
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
# Module-level metrics collector (shared across all requests)
|
|
25
|
+
_metrics = get_metrics_collector()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
async def list_mcp_servers(
|
|
29
|
+
internal_manager: "InternalToolRegistryManager | None" = Depends(get_internal_manager),
|
|
30
|
+
mcp_manager: "MCPClientManager | None" = Depends(get_mcp_manager),
|
|
31
|
+
) -> dict[str, Any]:
|
|
32
|
+
"""
|
|
33
|
+
List all configured MCP servers.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
internal_manager: Internal tool registry manager (injected)
|
|
37
|
+
mcp_manager: External MCP client manager (injected)
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
List of servers with connection status
|
|
41
|
+
"""
|
|
42
|
+
start_time = time.perf_counter()
|
|
43
|
+
_metrics.inc_counter("http_requests_total")
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
server_list = []
|
|
47
|
+
|
|
48
|
+
# Add internal servers (gobby-tasks, gobby-memory, etc.)
|
|
49
|
+
if internal_manager:
|
|
50
|
+
for registry in internal_manager.get_all_registries():
|
|
51
|
+
server_list.append(
|
|
52
|
+
{
|
|
53
|
+
"name": registry.name,
|
|
54
|
+
"state": "connected",
|
|
55
|
+
"connected": True,
|
|
56
|
+
"transport": "internal",
|
|
57
|
+
}
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
# Add external MCP servers
|
|
61
|
+
if mcp_manager:
|
|
62
|
+
for config in mcp_manager.server_configs:
|
|
63
|
+
health = mcp_manager.health.get(config.name)
|
|
64
|
+
is_connected = config.name in mcp_manager.connections
|
|
65
|
+
server_list.append(
|
|
66
|
+
{
|
|
67
|
+
"name": config.name,
|
|
68
|
+
"state": health.state.value if health else "unknown",
|
|
69
|
+
"connected": is_connected,
|
|
70
|
+
"transport": config.transport,
|
|
71
|
+
"enabled": config.enabled,
|
|
72
|
+
}
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
response_time_ms = (time.perf_counter() - start_time) * 1000
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
"servers": server_list,
|
|
79
|
+
"total_count": len(server_list),
|
|
80
|
+
"connected_count": len([s for s in server_list if s.get("connected")]),
|
|
81
|
+
"response_time_ms": response_time_ms,
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
except Exception as e:
|
|
85
|
+
_metrics.inc_counter("http_requests_errors_total")
|
|
86
|
+
logger.error(f"List MCP servers error: {e}", exc_info=True)
|
|
87
|
+
raise HTTPException(status_code=500, detail={"success": False, "error": str(e)}) from e
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
async def add_mcp_server(
|
|
91
|
+
request: Request,
|
|
92
|
+
server: "HTTPServer" = Depends(get_server),
|
|
93
|
+
) -> dict[str, Any]:
|
|
94
|
+
"""
|
|
95
|
+
Add a new MCP server configuration.
|
|
96
|
+
|
|
97
|
+
Request body:
|
|
98
|
+
{
|
|
99
|
+
"name": "my-server",
|
|
100
|
+
"transport": "http",
|
|
101
|
+
"url": "https://...",
|
|
102
|
+
"enabled": true
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
Success status
|
|
107
|
+
"""
|
|
108
|
+
_metrics.inc_counter("http_requests_total")
|
|
109
|
+
|
|
110
|
+
try:
|
|
111
|
+
body = await request.json()
|
|
112
|
+
name = body.get("name")
|
|
113
|
+
transport = body.get("transport")
|
|
114
|
+
|
|
115
|
+
if not name or not transport:
|
|
116
|
+
raise HTTPException(
|
|
117
|
+
status_code=400,
|
|
118
|
+
detail={"success": False, "error": "Required fields: name, transport"},
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
# Import here to avoid circular imports
|
|
122
|
+
from gobby.mcp_proxy.models import MCPServerConfig
|
|
123
|
+
from gobby.utils.project_context import get_project_context
|
|
124
|
+
|
|
125
|
+
project_ctx = get_project_context()
|
|
126
|
+
if not project_ctx or not project_ctx.get("id"):
|
|
127
|
+
raise HTTPException(
|
|
128
|
+
status_code=400,
|
|
129
|
+
detail={
|
|
130
|
+
"success": False,
|
|
131
|
+
"error": "No current project found. Run 'gobby init'.",
|
|
132
|
+
},
|
|
133
|
+
)
|
|
134
|
+
project_id = project_ctx["id"]
|
|
135
|
+
|
|
136
|
+
config = MCPServerConfig(
|
|
137
|
+
name=name,
|
|
138
|
+
project_id=project_id,
|
|
139
|
+
transport=transport,
|
|
140
|
+
url=body.get("url"),
|
|
141
|
+
command=body.get("command"),
|
|
142
|
+
args=body.get("args"),
|
|
143
|
+
env=body.get("env"),
|
|
144
|
+
headers=body.get("headers"),
|
|
145
|
+
enabled=body.get("enabled", True),
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
if server.mcp_manager is None:
|
|
149
|
+
raise HTTPException(
|
|
150
|
+
status_code=503,
|
|
151
|
+
detail={"success": False, "error": "MCP manager not available"},
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
await server.mcp_manager.add_server(config)
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
"success": True,
|
|
158
|
+
"message": f"Added MCP server: {name}",
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
except ValueError as e:
|
|
162
|
+
raise HTTPException(status_code=400, detail={"success": False, "error": str(e)}) from e
|
|
163
|
+
except HTTPException:
|
|
164
|
+
raise
|
|
165
|
+
except Exception as e:
|
|
166
|
+
_metrics.inc_counter("http_requests_errors_total")
|
|
167
|
+
logger.error(f"Add MCP server error: {e}", exc_info=True)
|
|
168
|
+
raise HTTPException(status_code=500, detail={"success": False, "error": str(e)}) from e
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
async def import_mcp_server(
|
|
172
|
+
request: Request,
|
|
173
|
+
server: "HTTPServer" = Depends(get_server),
|
|
174
|
+
) -> dict[str, Any]:
|
|
175
|
+
"""
|
|
176
|
+
Import MCP server(s) from various sources.
|
|
177
|
+
|
|
178
|
+
Request body:
|
|
179
|
+
{
|
|
180
|
+
"from_project": "other-project", # Import from project
|
|
181
|
+
"github_url": "https://...", # Import from GitHub
|
|
182
|
+
"query": "supabase mcp", # Search and import
|
|
183
|
+
"servers": ["name1", "name2"] # Specific servers to import
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
Import result with imported/skipped/failed lists
|
|
188
|
+
"""
|
|
189
|
+
_metrics.inc_counter("http_requests_total")
|
|
190
|
+
|
|
191
|
+
try:
|
|
192
|
+
body = await request.json()
|
|
193
|
+
from_project = body.get("from_project")
|
|
194
|
+
github_url = body.get("github_url")
|
|
195
|
+
query = body.get("query")
|
|
196
|
+
servers = body.get("servers")
|
|
197
|
+
|
|
198
|
+
if not from_project and not github_url and not query:
|
|
199
|
+
raise HTTPException(
|
|
200
|
+
status_code=400,
|
|
201
|
+
detail={
|
|
202
|
+
"success": False,
|
|
203
|
+
"error": "Specify at least one: from_project, github_url, or query",
|
|
204
|
+
},
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
# Get current project ID from context
|
|
208
|
+
from gobby.utils.project_context import get_project_context
|
|
209
|
+
|
|
210
|
+
project_ctx = get_project_context()
|
|
211
|
+
if not project_ctx or not project_ctx.get("id"):
|
|
212
|
+
raise HTTPException(
|
|
213
|
+
status_code=400,
|
|
214
|
+
detail={
|
|
215
|
+
"success": False,
|
|
216
|
+
"error": "No current project. Run 'gobby init' first.",
|
|
217
|
+
},
|
|
218
|
+
)
|
|
219
|
+
current_project_id = project_ctx["id"]
|
|
220
|
+
|
|
221
|
+
if not server.config:
|
|
222
|
+
raise HTTPException(
|
|
223
|
+
status_code=500,
|
|
224
|
+
detail={"success": False, "error": "Daemon configuration not available"},
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
# Create importer
|
|
228
|
+
from gobby.mcp_proxy.importer import MCPServerImporter
|
|
229
|
+
from gobby.storage.database import LocalDatabase
|
|
230
|
+
|
|
231
|
+
db = LocalDatabase()
|
|
232
|
+
importer = MCPServerImporter(
|
|
233
|
+
config=server.config,
|
|
234
|
+
db=db,
|
|
235
|
+
current_project_id=current_project_id,
|
|
236
|
+
mcp_client_manager=server.mcp_manager,
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
# Execute import based on source
|
|
240
|
+
# Note: validation above ensures at least one of these is truthy
|
|
241
|
+
if from_project:
|
|
242
|
+
result = await importer.import_from_project(
|
|
243
|
+
source_project=from_project,
|
|
244
|
+
servers=servers,
|
|
245
|
+
)
|
|
246
|
+
elif github_url:
|
|
247
|
+
result = await importer.import_from_github(github_url)
|
|
248
|
+
else:
|
|
249
|
+
# query must be truthy due to earlier validation
|
|
250
|
+
result = await importer.import_from_query(query)
|
|
251
|
+
|
|
252
|
+
return result
|
|
253
|
+
|
|
254
|
+
except HTTPException:
|
|
255
|
+
raise
|
|
256
|
+
except Exception as e:
|
|
257
|
+
_metrics.inc_counter("http_requests_errors_total")
|
|
258
|
+
logger.error(f"Import MCP server error: {e}", exc_info=True)
|
|
259
|
+
raise HTTPException(status_code=500, detail={"success": False, "error": str(e)}) from e
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
async def remove_mcp_server(
|
|
263
|
+
name: str,
|
|
264
|
+
server: "HTTPServer" = Depends(get_server),
|
|
265
|
+
) -> dict[str, Any]:
|
|
266
|
+
"""
|
|
267
|
+
Remove an MCP server configuration.
|
|
268
|
+
|
|
269
|
+
Args:
|
|
270
|
+
name: Server name to remove
|
|
271
|
+
|
|
272
|
+
Returns:
|
|
273
|
+
Success status
|
|
274
|
+
"""
|
|
275
|
+
_metrics.inc_counter("http_requests_total")
|
|
276
|
+
|
|
277
|
+
try:
|
|
278
|
+
if server.mcp_manager is None:
|
|
279
|
+
raise HTTPException(
|
|
280
|
+
status_code=503,
|
|
281
|
+
detail={"success": False, "error": "MCP manager not available"},
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
await server.mcp_manager.remove_server(name)
|
|
285
|
+
|
|
286
|
+
return {
|
|
287
|
+
"success": True,
|
|
288
|
+
"message": f"Removed MCP server: {name}",
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
except ValueError as e:
|
|
292
|
+
raise HTTPException(status_code=404, detail={"success": False, "error": str(e)}) from e
|
|
293
|
+
except Exception as e:
|
|
294
|
+
_metrics.inc_counter("http_requests_errors_total")
|
|
295
|
+
logger.error(f"Remove MCP server error: {e}", exc_info=True)
|
|
296
|
+
raise HTTPException(status_code=500, detail={"success": False, "error": str(e)}) from e
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
__all__ = [
|
|
300
|
+
"list_mcp_servers",
|
|
301
|
+
"add_mcp_server",
|
|
302
|
+
"import_mcp_server",
|
|
303
|
+
"remove_mcp_server",
|
|
304
|
+
]
|
|
@@ -73,7 +73,7 @@ def create_hooks_router(server: "HTTPServer") -> APIRouter:
|
|
|
73
73
|
# Select adapter based on source
|
|
74
74
|
from gobby.adapters.base import BaseAdapter
|
|
75
75
|
from gobby.adapters.claude_code import ClaudeCodeAdapter
|
|
76
|
-
from gobby.adapters.
|
|
76
|
+
from gobby.adapters.codex_impl.adapter import CodexNotifyAdapter
|
|
77
77
|
from gobby.adapters.gemini import GeminiAdapter
|
|
78
78
|
|
|
79
79
|
if source == "claude":
|