gobby 0.2.5__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 +3 -0
- gobby/adapters/__init__.py +30 -0
- gobby/adapters/base.py +93 -0
- gobby/adapters/claude_code.py +276 -0
- gobby/adapters/codex.py +1292 -0
- gobby/adapters/gemini.py +343 -0
- gobby/agents/__init__.py +37 -0
- gobby/agents/codex_session.py +120 -0
- gobby/agents/constants.py +112 -0
- gobby/agents/context.py +362 -0
- gobby/agents/definitions.py +133 -0
- gobby/agents/gemini_session.py +111 -0
- gobby/agents/registry.py +618 -0
- gobby/agents/runner.py +968 -0
- gobby/agents/session.py +259 -0
- gobby/agents/spawn.py +916 -0
- gobby/agents/spawners/__init__.py +77 -0
- gobby/agents/spawners/base.py +142 -0
- gobby/agents/spawners/cross_platform.py +266 -0
- gobby/agents/spawners/embedded.py +225 -0
- gobby/agents/spawners/headless.py +226 -0
- gobby/agents/spawners/linux.py +125 -0
- gobby/agents/spawners/macos.py +277 -0
- gobby/agents/spawners/windows.py +308 -0
- gobby/agents/tty_config.py +319 -0
- gobby/autonomous/__init__.py +32 -0
- gobby/autonomous/progress_tracker.py +447 -0
- gobby/autonomous/stop_registry.py +269 -0
- gobby/autonomous/stuck_detector.py +383 -0
- gobby/cli/__init__.py +67 -0
- gobby/cli/__main__.py +8 -0
- gobby/cli/agents.py +529 -0
- gobby/cli/artifacts.py +266 -0
- gobby/cli/daemon.py +329 -0
- gobby/cli/extensions.py +526 -0
- gobby/cli/github.py +263 -0
- gobby/cli/init.py +53 -0
- gobby/cli/install.py +614 -0
- gobby/cli/installers/__init__.py +37 -0
- gobby/cli/installers/antigravity.py +65 -0
- gobby/cli/installers/claude.py +363 -0
- gobby/cli/installers/codex.py +192 -0
- gobby/cli/installers/gemini.py +294 -0
- gobby/cli/installers/git_hooks.py +377 -0
- gobby/cli/installers/shared.py +737 -0
- gobby/cli/linear.py +250 -0
- gobby/cli/mcp.py +30 -0
- gobby/cli/mcp_proxy.py +698 -0
- gobby/cli/memory.py +304 -0
- gobby/cli/merge.py +384 -0
- gobby/cli/projects.py +79 -0
- gobby/cli/sessions.py +622 -0
- gobby/cli/tasks/__init__.py +30 -0
- gobby/cli/tasks/_utils.py +658 -0
- gobby/cli/tasks/ai.py +1025 -0
- gobby/cli/tasks/commits.py +169 -0
- gobby/cli/tasks/crud.py +685 -0
- gobby/cli/tasks/deps.py +135 -0
- gobby/cli/tasks/labels.py +63 -0
- gobby/cli/tasks/main.py +273 -0
- gobby/cli/tasks/search.py +178 -0
- gobby/cli/tui.py +34 -0
- gobby/cli/utils.py +513 -0
- gobby/cli/workflows.py +927 -0
- gobby/cli/worktrees.py +481 -0
- gobby/config/__init__.py +129 -0
- gobby/config/app.py +551 -0
- gobby/config/extensions.py +167 -0
- gobby/config/features.py +472 -0
- gobby/config/llm_providers.py +98 -0
- gobby/config/logging.py +66 -0
- gobby/config/mcp.py +346 -0
- gobby/config/persistence.py +247 -0
- gobby/config/servers.py +141 -0
- gobby/config/sessions.py +250 -0
- gobby/config/tasks.py +784 -0
- gobby/hooks/__init__.py +104 -0
- gobby/hooks/artifact_capture.py +213 -0
- gobby/hooks/broadcaster.py +243 -0
- gobby/hooks/event_handlers.py +723 -0
- gobby/hooks/events.py +218 -0
- gobby/hooks/git.py +169 -0
- gobby/hooks/health_monitor.py +171 -0
- gobby/hooks/hook_manager.py +856 -0
- gobby/hooks/hook_types.py +575 -0
- gobby/hooks/plugins.py +813 -0
- gobby/hooks/session_coordinator.py +396 -0
- gobby/hooks/verification_runner.py +268 -0
- gobby/hooks/webhooks.py +339 -0
- gobby/install/claude/commands/gobby/bug.md +51 -0
- gobby/install/claude/commands/gobby/chore.md +51 -0
- gobby/install/claude/commands/gobby/epic.md +52 -0
- gobby/install/claude/commands/gobby/eval.md +235 -0
- gobby/install/claude/commands/gobby/feat.md +49 -0
- gobby/install/claude/commands/gobby/nit.md +52 -0
- gobby/install/claude/commands/gobby/ref.md +52 -0
- gobby/install/claude/hooks/HOOK_SCHEMAS.md +632 -0
- gobby/install/claude/hooks/hook_dispatcher.py +364 -0
- gobby/install/claude/hooks/validate_settings.py +102 -0
- gobby/install/claude/hooks-template.json +118 -0
- gobby/install/codex/hooks/hook_dispatcher.py +153 -0
- gobby/install/codex/prompts/forget.md +7 -0
- gobby/install/codex/prompts/memories.md +7 -0
- gobby/install/codex/prompts/recall.md +7 -0
- gobby/install/codex/prompts/remember.md +13 -0
- gobby/install/gemini/hooks/hook_dispatcher.py +268 -0
- gobby/install/gemini/hooks-template.json +138 -0
- gobby/install/shared/plugins/code_guardian.py +456 -0
- gobby/install/shared/plugins/example_notify.py +331 -0
- gobby/integrations/__init__.py +10 -0
- gobby/integrations/github.py +145 -0
- gobby/integrations/linear.py +145 -0
- gobby/llm/__init__.py +40 -0
- gobby/llm/base.py +120 -0
- gobby/llm/claude.py +578 -0
- gobby/llm/claude_executor.py +503 -0
- gobby/llm/codex.py +322 -0
- gobby/llm/codex_executor.py +513 -0
- gobby/llm/executor.py +316 -0
- gobby/llm/factory.py +34 -0
- gobby/llm/gemini.py +258 -0
- gobby/llm/gemini_executor.py +339 -0
- gobby/llm/litellm.py +287 -0
- gobby/llm/litellm_executor.py +303 -0
- gobby/llm/resolver.py +499 -0
- gobby/llm/service.py +236 -0
- gobby/mcp_proxy/__init__.py +29 -0
- gobby/mcp_proxy/actions.py +175 -0
- gobby/mcp_proxy/daemon_control.py +198 -0
- gobby/mcp_proxy/importer.py +436 -0
- gobby/mcp_proxy/lazy.py +325 -0
- gobby/mcp_proxy/manager.py +798 -0
- gobby/mcp_proxy/metrics.py +609 -0
- gobby/mcp_proxy/models.py +139 -0
- gobby/mcp_proxy/registries.py +215 -0
- gobby/mcp_proxy/schema_hash.py +381 -0
- gobby/mcp_proxy/semantic_search.py +706 -0
- gobby/mcp_proxy/server.py +549 -0
- gobby/mcp_proxy/services/__init__.py +0 -0
- gobby/mcp_proxy/services/fallback.py +306 -0
- gobby/mcp_proxy/services/recommendation.py +224 -0
- gobby/mcp_proxy/services/server_mgmt.py +214 -0
- gobby/mcp_proxy/services/system.py +72 -0
- gobby/mcp_proxy/services/tool_filter.py +231 -0
- gobby/mcp_proxy/services/tool_proxy.py +309 -0
- gobby/mcp_proxy/stdio.py +565 -0
- gobby/mcp_proxy/tools/__init__.py +27 -0
- gobby/mcp_proxy/tools/agents.py +1103 -0
- gobby/mcp_proxy/tools/artifacts.py +207 -0
- gobby/mcp_proxy/tools/hub.py +335 -0
- gobby/mcp_proxy/tools/internal.py +337 -0
- gobby/mcp_proxy/tools/memory.py +543 -0
- gobby/mcp_proxy/tools/merge.py +422 -0
- gobby/mcp_proxy/tools/metrics.py +283 -0
- gobby/mcp_proxy/tools/orchestration/__init__.py +23 -0
- gobby/mcp_proxy/tools/orchestration/cleanup.py +619 -0
- gobby/mcp_proxy/tools/orchestration/monitor.py +380 -0
- gobby/mcp_proxy/tools/orchestration/orchestrate.py +746 -0
- gobby/mcp_proxy/tools/orchestration/review.py +736 -0
- gobby/mcp_proxy/tools/orchestration/utils.py +16 -0
- gobby/mcp_proxy/tools/session_messages.py +1056 -0
- gobby/mcp_proxy/tools/task_dependencies.py +219 -0
- gobby/mcp_proxy/tools/task_expansion.py +591 -0
- gobby/mcp_proxy/tools/task_github.py +393 -0
- gobby/mcp_proxy/tools/task_linear.py +379 -0
- gobby/mcp_proxy/tools/task_orchestration.py +77 -0
- gobby/mcp_proxy/tools/task_readiness.py +522 -0
- gobby/mcp_proxy/tools/task_sync.py +351 -0
- gobby/mcp_proxy/tools/task_validation.py +843 -0
- gobby/mcp_proxy/tools/tasks/__init__.py +25 -0
- gobby/mcp_proxy/tools/tasks/_context.py +112 -0
- gobby/mcp_proxy/tools/tasks/_crud.py +516 -0
- gobby/mcp_proxy/tools/tasks/_factory.py +176 -0
- gobby/mcp_proxy/tools/tasks/_helpers.py +129 -0
- gobby/mcp_proxy/tools/tasks/_lifecycle.py +517 -0
- gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +301 -0
- gobby/mcp_proxy/tools/tasks/_resolution.py +55 -0
- gobby/mcp_proxy/tools/tasks/_search.py +215 -0
- gobby/mcp_proxy/tools/tasks/_session.py +125 -0
- gobby/mcp_proxy/tools/workflows.py +973 -0
- gobby/mcp_proxy/tools/worktrees.py +1264 -0
- gobby/mcp_proxy/transports/__init__.py +0 -0
- gobby/mcp_proxy/transports/base.py +95 -0
- gobby/mcp_proxy/transports/factory.py +44 -0
- gobby/mcp_proxy/transports/http.py +139 -0
- gobby/mcp_proxy/transports/stdio.py +213 -0
- gobby/mcp_proxy/transports/websocket.py +136 -0
- gobby/memory/backends/__init__.py +116 -0
- gobby/memory/backends/mem0.py +408 -0
- gobby/memory/backends/memu.py +485 -0
- gobby/memory/backends/null.py +111 -0
- gobby/memory/backends/openmemory.py +537 -0
- gobby/memory/backends/sqlite.py +304 -0
- gobby/memory/context.py +87 -0
- gobby/memory/manager.py +1001 -0
- gobby/memory/protocol.py +451 -0
- gobby/memory/search/__init__.py +66 -0
- gobby/memory/search/text.py +127 -0
- gobby/memory/viz.py +258 -0
- gobby/prompts/__init__.py +13 -0
- gobby/prompts/defaults/expansion/system.md +119 -0
- gobby/prompts/defaults/expansion/user.md +48 -0
- gobby/prompts/defaults/external_validation/agent.md +72 -0
- gobby/prompts/defaults/external_validation/external.md +63 -0
- gobby/prompts/defaults/external_validation/spawn.md +83 -0
- gobby/prompts/defaults/external_validation/system.md +6 -0
- gobby/prompts/defaults/features/import_mcp.md +22 -0
- gobby/prompts/defaults/features/import_mcp_github.md +17 -0
- gobby/prompts/defaults/features/import_mcp_search.md +16 -0
- gobby/prompts/defaults/features/recommend_tools.md +32 -0
- gobby/prompts/defaults/features/recommend_tools_hybrid.md +35 -0
- gobby/prompts/defaults/features/recommend_tools_llm.md +30 -0
- gobby/prompts/defaults/features/server_description.md +20 -0
- gobby/prompts/defaults/features/server_description_system.md +6 -0
- gobby/prompts/defaults/features/task_description.md +31 -0
- gobby/prompts/defaults/features/task_description_system.md +6 -0
- gobby/prompts/defaults/features/tool_summary.md +17 -0
- gobby/prompts/defaults/features/tool_summary_system.md +6 -0
- gobby/prompts/defaults/research/step.md +58 -0
- gobby/prompts/defaults/validation/criteria.md +47 -0
- gobby/prompts/defaults/validation/validate.md +38 -0
- gobby/prompts/loader.py +346 -0
- gobby/prompts/models.py +113 -0
- gobby/py.typed +0 -0
- gobby/runner.py +488 -0
- gobby/search/__init__.py +23 -0
- gobby/search/protocol.py +104 -0
- gobby/search/tfidf.py +232 -0
- gobby/servers/__init__.py +7 -0
- gobby/servers/http.py +636 -0
- gobby/servers/models.py +31 -0
- gobby/servers/routes/__init__.py +23 -0
- gobby/servers/routes/admin.py +416 -0
- gobby/servers/routes/dependencies.py +118 -0
- gobby/servers/routes/mcp/__init__.py +24 -0
- gobby/servers/routes/mcp/hooks.py +135 -0
- gobby/servers/routes/mcp/plugins.py +121 -0
- gobby/servers/routes/mcp/tools.py +1337 -0
- gobby/servers/routes/mcp/webhooks.py +159 -0
- gobby/servers/routes/sessions.py +582 -0
- gobby/servers/websocket.py +766 -0
- gobby/sessions/__init__.py +13 -0
- gobby/sessions/analyzer.py +322 -0
- gobby/sessions/lifecycle.py +240 -0
- gobby/sessions/manager.py +563 -0
- gobby/sessions/processor.py +225 -0
- gobby/sessions/summary.py +532 -0
- gobby/sessions/transcripts/__init__.py +41 -0
- gobby/sessions/transcripts/base.py +125 -0
- gobby/sessions/transcripts/claude.py +386 -0
- gobby/sessions/transcripts/codex.py +143 -0
- gobby/sessions/transcripts/gemini.py +195 -0
- gobby/storage/__init__.py +21 -0
- gobby/storage/agents.py +409 -0
- gobby/storage/artifact_classifier.py +341 -0
- gobby/storage/artifacts.py +285 -0
- gobby/storage/compaction.py +67 -0
- gobby/storage/database.py +357 -0
- gobby/storage/inter_session_messages.py +194 -0
- gobby/storage/mcp.py +680 -0
- gobby/storage/memories.py +562 -0
- gobby/storage/merge_resolutions.py +550 -0
- gobby/storage/migrations.py +860 -0
- gobby/storage/migrations_legacy.py +1359 -0
- gobby/storage/projects.py +166 -0
- gobby/storage/session_messages.py +251 -0
- gobby/storage/session_tasks.py +97 -0
- gobby/storage/sessions.py +817 -0
- gobby/storage/task_dependencies.py +223 -0
- gobby/storage/tasks/__init__.py +42 -0
- gobby/storage/tasks/_aggregates.py +180 -0
- gobby/storage/tasks/_crud.py +449 -0
- gobby/storage/tasks/_id.py +104 -0
- gobby/storage/tasks/_lifecycle.py +311 -0
- gobby/storage/tasks/_manager.py +889 -0
- gobby/storage/tasks/_models.py +300 -0
- gobby/storage/tasks/_ordering.py +119 -0
- gobby/storage/tasks/_path_cache.py +110 -0
- gobby/storage/tasks/_queries.py +343 -0
- gobby/storage/tasks/_search.py +143 -0
- gobby/storage/workflow_audit.py +393 -0
- gobby/storage/worktrees.py +547 -0
- gobby/sync/__init__.py +29 -0
- gobby/sync/github.py +333 -0
- gobby/sync/linear.py +304 -0
- gobby/sync/memories.py +284 -0
- gobby/sync/tasks.py +641 -0
- gobby/tasks/__init__.py +8 -0
- gobby/tasks/build_verification.py +193 -0
- gobby/tasks/commits.py +633 -0
- gobby/tasks/context.py +747 -0
- gobby/tasks/criteria.py +342 -0
- gobby/tasks/enhanced_validator.py +226 -0
- gobby/tasks/escalation.py +263 -0
- gobby/tasks/expansion.py +626 -0
- gobby/tasks/external_validator.py +764 -0
- gobby/tasks/issue_extraction.py +171 -0
- gobby/tasks/prompts/expand.py +327 -0
- gobby/tasks/research.py +421 -0
- gobby/tasks/tdd.py +352 -0
- gobby/tasks/tree_builder.py +263 -0
- gobby/tasks/validation.py +712 -0
- gobby/tasks/validation_history.py +357 -0
- gobby/tasks/validation_models.py +89 -0
- gobby/tools/__init__.py +0 -0
- gobby/tools/summarizer.py +170 -0
- gobby/tui/__init__.py +5 -0
- gobby/tui/api_client.py +281 -0
- gobby/tui/app.py +327 -0
- gobby/tui/screens/__init__.py +25 -0
- gobby/tui/screens/agents.py +333 -0
- gobby/tui/screens/chat.py +450 -0
- gobby/tui/screens/dashboard.py +377 -0
- gobby/tui/screens/memory.py +305 -0
- gobby/tui/screens/metrics.py +231 -0
- gobby/tui/screens/orchestrator.py +904 -0
- gobby/tui/screens/sessions.py +412 -0
- gobby/tui/screens/tasks.py +442 -0
- gobby/tui/screens/workflows.py +289 -0
- gobby/tui/screens/worktrees.py +174 -0
- gobby/tui/widgets/__init__.py +21 -0
- gobby/tui/widgets/chat.py +210 -0
- gobby/tui/widgets/conductor.py +104 -0
- gobby/tui/widgets/menu.py +132 -0
- gobby/tui/widgets/message_panel.py +160 -0
- gobby/tui/widgets/review_gate.py +224 -0
- gobby/tui/widgets/task_tree.py +99 -0
- gobby/tui/widgets/token_budget.py +166 -0
- gobby/tui/ws_client.py +258 -0
- gobby/utils/__init__.py +3 -0
- gobby/utils/daemon_client.py +235 -0
- gobby/utils/git.py +222 -0
- gobby/utils/id.py +38 -0
- gobby/utils/json_helpers.py +161 -0
- gobby/utils/logging.py +376 -0
- gobby/utils/machine_id.py +135 -0
- gobby/utils/metrics.py +589 -0
- gobby/utils/project_context.py +182 -0
- gobby/utils/project_init.py +263 -0
- gobby/utils/status.py +256 -0
- gobby/utils/validation.py +80 -0
- gobby/utils/version.py +23 -0
- gobby/workflows/__init__.py +4 -0
- gobby/workflows/actions.py +1310 -0
- gobby/workflows/approval_flow.py +138 -0
- gobby/workflows/artifact_actions.py +103 -0
- gobby/workflows/audit_helpers.py +110 -0
- gobby/workflows/autonomous_actions.py +286 -0
- gobby/workflows/context_actions.py +394 -0
- gobby/workflows/definitions.py +130 -0
- gobby/workflows/detection_helpers.py +208 -0
- gobby/workflows/engine.py +485 -0
- gobby/workflows/evaluator.py +669 -0
- gobby/workflows/git_utils.py +96 -0
- gobby/workflows/hooks.py +169 -0
- gobby/workflows/lifecycle_evaluator.py +613 -0
- gobby/workflows/llm_actions.py +70 -0
- gobby/workflows/loader.py +333 -0
- gobby/workflows/mcp_actions.py +60 -0
- gobby/workflows/memory_actions.py +272 -0
- gobby/workflows/premature_stop.py +164 -0
- gobby/workflows/session_actions.py +139 -0
- gobby/workflows/state_actions.py +123 -0
- gobby/workflows/state_manager.py +104 -0
- gobby/workflows/stop_signal_actions.py +163 -0
- gobby/workflows/summary_actions.py +344 -0
- gobby/workflows/task_actions.py +249 -0
- gobby/workflows/task_enforcement_actions.py +901 -0
- gobby/workflows/templates.py +52 -0
- gobby/workflows/todo_actions.py +84 -0
- gobby/workflows/webhook.py +223 -0
- gobby/workflows/webhook_executor.py +399 -0
- gobby/worktrees/__init__.py +5 -0
- gobby/worktrees/git.py +690 -0
- gobby/worktrees/merge/__init__.py +20 -0
- gobby/worktrees/merge/conflict_parser.py +177 -0
- gobby/worktrees/merge/resolver.py +485 -0
- gobby-0.2.5.dist-info/METADATA +351 -0
- gobby-0.2.5.dist-info/RECORD +383 -0
- gobby-0.2.5.dist-info/WHEEL +5 -0
- gobby-0.2.5.dist-info/entry_points.txt +2 -0
- gobby-0.2.5.dist-info/licenses/LICENSE.md +193 -0
- gobby-0.2.5.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
"""MCP server import functionality."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import re
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
|
|
7
|
+
from gobby.config.app import DaemonConfig
|
|
8
|
+
from gobby.storage.database import DatabaseProtocol
|
|
9
|
+
from gobby.storage.mcp import LocalMCPManager
|
|
10
|
+
from gobby.storage.projects import LocalProjectManager
|
|
11
|
+
from gobby.utils.json_helpers import extract_json_object
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from gobby.mcp_proxy.manager import MCPClientManager
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
# Pattern to detect placeholder secrets like <YOUR_API_KEY>
|
|
19
|
+
SECRET_PLACEHOLDER_PATTERN = re.compile(r"<YOUR_[A-Z0-9_]+>")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class MCPServerImporter:
|
|
23
|
+
"""Handles importing MCP servers from various sources."""
|
|
24
|
+
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
config: DaemonConfig,
|
|
28
|
+
db: DatabaseProtocol,
|
|
29
|
+
current_project_id: str,
|
|
30
|
+
mcp_client_manager: "MCPClientManager | None" = None,
|
|
31
|
+
):
|
|
32
|
+
"""
|
|
33
|
+
Initialize the importer.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
config: Daemon configuration
|
|
37
|
+
db: Database connection
|
|
38
|
+
current_project_id: ID of the current project to import into
|
|
39
|
+
mcp_client_manager: Optional MCP client manager for live connections
|
|
40
|
+
"""
|
|
41
|
+
self.config = config
|
|
42
|
+
self.db = db
|
|
43
|
+
self.current_project_id = current_project_id
|
|
44
|
+
self.mcp_db_manager = LocalMCPManager(db)
|
|
45
|
+
self.project_manager = LocalProjectManager(db)
|
|
46
|
+
self.mcp_client_manager = mcp_client_manager
|
|
47
|
+
self.import_config = config.get_import_mcp_server_config()
|
|
48
|
+
|
|
49
|
+
async def import_from_project(
|
|
50
|
+
self,
|
|
51
|
+
source_project: str,
|
|
52
|
+
servers: list[str] | None = None,
|
|
53
|
+
) -> dict[str, Any]:
|
|
54
|
+
"""
|
|
55
|
+
Import MCP servers from another Gobby project.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
source_project: Source project name or ID
|
|
59
|
+
servers: Optional list of server names to import (imports all if None)
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
Result dict with imported servers or error
|
|
63
|
+
"""
|
|
64
|
+
# Resolve source project - try by name first, then by ID
|
|
65
|
+
project = self.project_manager.get_by_name(source_project)
|
|
66
|
+
if not project:
|
|
67
|
+
project = self.project_manager.get(source_project)
|
|
68
|
+
|
|
69
|
+
if not project:
|
|
70
|
+
# List available projects for helpful error message
|
|
71
|
+
available = self.project_manager.list()
|
|
72
|
+
project_names = [p.name for p in available]
|
|
73
|
+
return {
|
|
74
|
+
"success": False,
|
|
75
|
+
"error": f"Project '{source_project}' not found",
|
|
76
|
+
"available_projects": project_names,
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
# Get servers from source project
|
|
80
|
+
source_servers = self.mcp_db_manager.list_servers(
|
|
81
|
+
project_id=project.id,
|
|
82
|
+
enabled_only=False, # Include disabled servers too
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
if not source_servers:
|
|
86
|
+
return {
|
|
87
|
+
"success": False,
|
|
88
|
+
"error": f"No MCP servers found in project '{project.name}'",
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
# Filter by server names if specified
|
|
92
|
+
if servers:
|
|
93
|
+
servers_lower = [s.lower() for s in servers]
|
|
94
|
+
source_servers = [s for s in source_servers if s.name.lower() in servers_lower]
|
|
95
|
+
if not source_servers:
|
|
96
|
+
return {
|
|
97
|
+
"success": False,
|
|
98
|
+
"error": f"None of the specified servers found in project '{project.name}'",
|
|
99
|
+
"requested": servers,
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
# Get existing servers in current project to skip duplicates
|
|
103
|
+
existing_servers = self.mcp_db_manager.list_servers(
|
|
104
|
+
project_id=self.current_project_id,
|
|
105
|
+
enabled_only=False,
|
|
106
|
+
)
|
|
107
|
+
existing_names = {s.name.lower() for s in existing_servers}
|
|
108
|
+
|
|
109
|
+
# Import each server
|
|
110
|
+
imported = []
|
|
111
|
+
skipped = []
|
|
112
|
+
failed = []
|
|
113
|
+
|
|
114
|
+
for server in source_servers:
|
|
115
|
+
if server.name.lower() in existing_names:
|
|
116
|
+
skipped.append(server.name)
|
|
117
|
+
continue
|
|
118
|
+
|
|
119
|
+
# Add server using action (connects and saves) or just save to db
|
|
120
|
+
add_result = await self._add_server(
|
|
121
|
+
name=server.name,
|
|
122
|
+
transport=server.transport,
|
|
123
|
+
url=server.url,
|
|
124
|
+
command=server.command,
|
|
125
|
+
args=server.args,
|
|
126
|
+
env=server.env,
|
|
127
|
+
headers=server.headers,
|
|
128
|
+
enabled=server.enabled,
|
|
129
|
+
description=server.description,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
if add_result.get("success"):
|
|
133
|
+
imported.append(server.name)
|
|
134
|
+
else:
|
|
135
|
+
failed.append({"name": server.name, "error": add_result.get("error")})
|
|
136
|
+
|
|
137
|
+
result: dict[str, Any] = {
|
|
138
|
+
"success": len(imported) > 0 or len(failed) == 0,
|
|
139
|
+
"imported": imported,
|
|
140
|
+
"message": f"Imported {len(imported)} server(s) from project '{project.name}'",
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if skipped:
|
|
144
|
+
result["skipped"] = skipped
|
|
145
|
+
result["message"] += f" (skipped {len(skipped)} existing)"
|
|
146
|
+
|
|
147
|
+
if failed:
|
|
148
|
+
result["failed"] = failed
|
|
149
|
+
|
|
150
|
+
return result
|
|
151
|
+
|
|
152
|
+
async def import_from_github(self, github_url: str) -> dict[str, Any]:
|
|
153
|
+
"""
|
|
154
|
+
Import MCP server from GitHub repository.
|
|
155
|
+
|
|
156
|
+
Uses Claude Agent SDK to fetch and parse the README.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
github_url: GitHub repository URL
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
Result dict with config (may need user input for secrets)
|
|
163
|
+
"""
|
|
164
|
+
if not self.import_config.enabled:
|
|
165
|
+
return {
|
|
166
|
+
"success": False,
|
|
167
|
+
"error": "MCP server import is disabled in configuration",
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
try:
|
|
171
|
+
from claude_agent_sdk import AssistantMessage, ClaudeAgentOptions, TextBlock, query
|
|
172
|
+
|
|
173
|
+
# Build prompt to fetch and extract config
|
|
174
|
+
prompt = self.import_config.github_fetch_prompt.format(github_url=github_url)
|
|
175
|
+
|
|
176
|
+
options = ClaudeAgentOptions(
|
|
177
|
+
system_prompt=self.import_config.prompt,
|
|
178
|
+
max_turns=3,
|
|
179
|
+
model=self.import_config.model,
|
|
180
|
+
allowed_tools=["WebFetch"],
|
|
181
|
+
permission_mode="default",
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
# Run query
|
|
185
|
+
result_text = ""
|
|
186
|
+
async for message in query(prompt=prompt, options=options):
|
|
187
|
+
if isinstance(message, AssistantMessage):
|
|
188
|
+
for block in message.content:
|
|
189
|
+
if isinstance(block, TextBlock):
|
|
190
|
+
result_text += block.text
|
|
191
|
+
|
|
192
|
+
# Parse and add if no secrets needed
|
|
193
|
+
return await self._parse_and_add_config(result_text)
|
|
194
|
+
|
|
195
|
+
except Exception as e:
|
|
196
|
+
logger.error(f"Failed to import from GitHub: {e}")
|
|
197
|
+
return {
|
|
198
|
+
"success": False,
|
|
199
|
+
"error": str(e),
|
|
200
|
+
"error_type": type(e).__name__,
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async def import_from_query(self, search_query: str) -> dict[str, Any]:
|
|
204
|
+
"""
|
|
205
|
+
Import MCP server by searching for it.
|
|
206
|
+
|
|
207
|
+
Uses Claude Agent SDK to search and extract configuration.
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
search_query: Natural language search query
|
|
211
|
+
|
|
212
|
+
Returns:
|
|
213
|
+
Result dict with config (may need user input for secrets)
|
|
214
|
+
"""
|
|
215
|
+
if not self.import_config.enabled:
|
|
216
|
+
return {
|
|
217
|
+
"success": False,
|
|
218
|
+
"error": "MCP server import is disabled in configuration",
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
try:
|
|
222
|
+
from claude_agent_sdk import AssistantMessage, ClaudeAgentOptions, TextBlock, query
|
|
223
|
+
|
|
224
|
+
# Build prompt to search and extract config
|
|
225
|
+
prompt = self.import_config.search_fetch_prompt.format(search_query=search_query)
|
|
226
|
+
|
|
227
|
+
options = ClaudeAgentOptions(
|
|
228
|
+
system_prompt=self.import_config.prompt,
|
|
229
|
+
max_turns=5, # More turns for search + fetch
|
|
230
|
+
model=self.import_config.model,
|
|
231
|
+
allowed_tools=["WebSearch", "WebFetch"],
|
|
232
|
+
permission_mode="default",
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
# Run query
|
|
236
|
+
result_text = ""
|
|
237
|
+
async for message in query(prompt=prompt, options=options):
|
|
238
|
+
if isinstance(message, AssistantMessage):
|
|
239
|
+
for block in message.content:
|
|
240
|
+
if isinstance(block, TextBlock):
|
|
241
|
+
result_text += block.text
|
|
242
|
+
|
|
243
|
+
# Parse and add if no secrets needed
|
|
244
|
+
return await self._parse_and_add_config(result_text)
|
|
245
|
+
|
|
246
|
+
except Exception as e:
|
|
247
|
+
logger.error(f"Failed to import from query: {e}")
|
|
248
|
+
return {
|
|
249
|
+
"success": False,
|
|
250
|
+
"error": str(e),
|
|
251
|
+
"error_type": type(e).__name__,
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async def _add_server(
|
|
255
|
+
self,
|
|
256
|
+
name: str,
|
|
257
|
+
transport: str,
|
|
258
|
+
url: str | None = None,
|
|
259
|
+
command: str | None = None,
|
|
260
|
+
args: list[str] | None = None,
|
|
261
|
+
env: dict[str, str] | None = None,
|
|
262
|
+
headers: dict[str, str] | None = None,
|
|
263
|
+
enabled: bool = True,
|
|
264
|
+
description: str | None = None,
|
|
265
|
+
) -> dict[str, Any]:
|
|
266
|
+
"""
|
|
267
|
+
Add an MCP server using the action (connects + saves) or db-only fallback.
|
|
268
|
+
|
|
269
|
+
Args:
|
|
270
|
+
name: Server name
|
|
271
|
+
transport: Transport type
|
|
272
|
+
url: Server URL (for http/websocket)
|
|
273
|
+
command: Command (for stdio)
|
|
274
|
+
args: Command args (for stdio)
|
|
275
|
+
env: Environment variables
|
|
276
|
+
headers: HTTP headers
|
|
277
|
+
enabled: Whether server is enabled
|
|
278
|
+
description: Server description
|
|
279
|
+
|
|
280
|
+
Returns:
|
|
281
|
+
Result dict with success status
|
|
282
|
+
"""
|
|
283
|
+
try:
|
|
284
|
+
if self.mcp_client_manager:
|
|
285
|
+
# Use the action which connects and saves
|
|
286
|
+
from gobby.mcp_proxy.actions import add_mcp_server
|
|
287
|
+
|
|
288
|
+
result: dict[str, Any] = await add_mcp_server(
|
|
289
|
+
mcp_manager=self.mcp_client_manager,
|
|
290
|
+
name=name,
|
|
291
|
+
transport=transport,
|
|
292
|
+
project_id=self.current_project_id,
|
|
293
|
+
url=url,
|
|
294
|
+
headers=headers,
|
|
295
|
+
command=command,
|
|
296
|
+
args=args,
|
|
297
|
+
env=env,
|
|
298
|
+
enabled=enabled,
|
|
299
|
+
description=description,
|
|
300
|
+
)
|
|
301
|
+
return result
|
|
302
|
+
else:
|
|
303
|
+
# Fallback to db-only (won't be connected until restart)
|
|
304
|
+
self.mcp_db_manager.upsert(
|
|
305
|
+
name=name,
|
|
306
|
+
transport=transport,
|
|
307
|
+
project_id=self.current_project_id,
|
|
308
|
+
url=url,
|
|
309
|
+
command=command,
|
|
310
|
+
args=args,
|
|
311
|
+
env=env,
|
|
312
|
+
headers=headers,
|
|
313
|
+
enabled=enabled,
|
|
314
|
+
description=description,
|
|
315
|
+
)
|
|
316
|
+
return {
|
|
317
|
+
"success": True,
|
|
318
|
+
"imported": [name],
|
|
319
|
+
"message": f"Successfully added MCP server '{name}' (restart daemon to connect)",
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
except Exception as e:
|
|
323
|
+
logger.error(f"Failed to add server '{name}': {e}")
|
|
324
|
+
return {
|
|
325
|
+
"success": False,
|
|
326
|
+
"name": name,
|
|
327
|
+
"error": str(e),
|
|
328
|
+
"error_type": type(e).__name__,
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
async def _parse_and_add_config(self, result_text: str) -> dict[str, Any]:
|
|
332
|
+
"""
|
|
333
|
+
Parse LLM response and add server if no secrets needed.
|
|
334
|
+
|
|
335
|
+
Args:
|
|
336
|
+
result_text: Raw text from LLM
|
|
337
|
+
|
|
338
|
+
Returns:
|
|
339
|
+
Success result if added, or needs_configuration if secrets required
|
|
340
|
+
"""
|
|
341
|
+
# Try to extract JSON from the response
|
|
342
|
+
config = self._extract_json(result_text)
|
|
343
|
+
|
|
344
|
+
if not config:
|
|
345
|
+
return {
|
|
346
|
+
"success": False,
|
|
347
|
+
"error": "Could not extract valid configuration from documentation",
|
|
348
|
+
"raw_response": result_text[:1000], # Include first 1000 chars for debugging
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
# Check for missing secrets
|
|
352
|
+
missing = self._find_missing_secrets(config)
|
|
353
|
+
instructions = config.pop("instructions", None)
|
|
354
|
+
|
|
355
|
+
if missing:
|
|
356
|
+
# Secrets needed - return config for user to fill in
|
|
357
|
+
result: dict[str, Any] = {
|
|
358
|
+
"status": "needs_configuration",
|
|
359
|
+
"config": config,
|
|
360
|
+
"missing": missing,
|
|
361
|
+
}
|
|
362
|
+
if instructions:
|
|
363
|
+
result["instructions"] = instructions
|
|
364
|
+
return result
|
|
365
|
+
|
|
366
|
+
# No secrets needed - add the server directly
|
|
367
|
+
name = config.get("name")
|
|
368
|
+
transport = config.get("transport")
|
|
369
|
+
|
|
370
|
+
if not name or not transport:
|
|
371
|
+
return {
|
|
372
|
+
"success": False,
|
|
373
|
+
"error": "Extracted config missing required fields: name or transport",
|
|
374
|
+
"config": config,
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
return await self._add_server(
|
|
378
|
+
name=name,
|
|
379
|
+
transport=transport,
|
|
380
|
+
url=config.get("url"),
|
|
381
|
+
command=config.get("command"),
|
|
382
|
+
args=config.get("args"),
|
|
383
|
+
env=config.get("env"),
|
|
384
|
+
headers=config.get("headers"),
|
|
385
|
+
enabled=config.get("enabled", True),
|
|
386
|
+
description=config.get("description"),
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
def _extract_json(self, text: str) -> dict[str, Any] | None:
|
|
390
|
+
"""
|
|
391
|
+
Extract JSON object from text.
|
|
392
|
+
|
|
393
|
+
Handles JSON in code blocks or raw JSON.
|
|
394
|
+
|
|
395
|
+
Args:
|
|
396
|
+
text: Text potentially containing JSON
|
|
397
|
+
|
|
398
|
+
Returns:
|
|
399
|
+
Parsed JSON dict or None
|
|
400
|
+
"""
|
|
401
|
+
result = extract_json_object(text)
|
|
402
|
+
if result is None:
|
|
403
|
+
return None
|
|
404
|
+
|
|
405
|
+
# Validate it looks like a server config
|
|
406
|
+
if "name" in result or "transport" in result:
|
|
407
|
+
return result
|
|
408
|
+
|
|
409
|
+
return None
|
|
410
|
+
|
|
411
|
+
def _find_missing_secrets(self, config: dict[str, Any]) -> list[str]:
|
|
412
|
+
"""
|
|
413
|
+
Find placeholder secrets in config.
|
|
414
|
+
|
|
415
|
+
Args:
|
|
416
|
+
config: Server configuration dict
|
|
417
|
+
|
|
418
|
+
Returns:
|
|
419
|
+
List of placeholder secret names
|
|
420
|
+
"""
|
|
421
|
+
missing = []
|
|
422
|
+
|
|
423
|
+
def check_value(value: Any, path: str = "") -> None:
|
|
424
|
+
if isinstance(value, str):
|
|
425
|
+
match = SECRET_PLACEHOLDER_PATTERN.search(value)
|
|
426
|
+
if match:
|
|
427
|
+
missing.append(match.group(0))
|
|
428
|
+
elif isinstance(value, dict):
|
|
429
|
+
for k, v in value.items():
|
|
430
|
+
check_value(v, f"{path}.{k}" if path else k)
|
|
431
|
+
elif isinstance(value, list):
|
|
432
|
+
for i, v in enumerate(value):
|
|
433
|
+
check_value(v, f"{path}[{i}]")
|
|
434
|
+
|
|
435
|
+
check_value(config)
|
|
436
|
+
return missing
|