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
gobby/storage/mcp.py
ADDED
|
@@ -0,0 +1,680 @@
|
|
|
1
|
+
"""Local MCP server and tool storage manager."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
import uuid
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from datetime import UTC, datetime
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from gobby.storage.database import DatabaseProtocol
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class MCPServer:
|
|
18
|
+
"""MCP server configuration model."""
|
|
19
|
+
|
|
20
|
+
id: str
|
|
21
|
+
name: str
|
|
22
|
+
transport: str
|
|
23
|
+
url: str | None
|
|
24
|
+
command: str | None
|
|
25
|
+
args: list[str] | None
|
|
26
|
+
env: dict[str, str] | None
|
|
27
|
+
headers: dict[str, str] | None
|
|
28
|
+
enabled: bool
|
|
29
|
+
description: str | None
|
|
30
|
+
created_at: str
|
|
31
|
+
updated_at: str
|
|
32
|
+
project_id: str # Required - all servers must belong to a project
|
|
33
|
+
|
|
34
|
+
@classmethod
|
|
35
|
+
def from_row(cls, row: Any) -> "MCPServer":
|
|
36
|
+
"""Create MCPServer from database row."""
|
|
37
|
+
return cls(
|
|
38
|
+
id=row["id"],
|
|
39
|
+
name=row["name"],
|
|
40
|
+
transport=row["transport"],
|
|
41
|
+
url=row["url"],
|
|
42
|
+
command=row["command"],
|
|
43
|
+
args=json.loads(row["args"]) if row["args"] else None,
|
|
44
|
+
env=json.loads(row["env"]) if row["env"] else None,
|
|
45
|
+
headers=json.loads(row["headers"]) if row["headers"] else None,
|
|
46
|
+
enabled=bool(row["enabled"]),
|
|
47
|
+
description=row["description"],
|
|
48
|
+
created_at=row["created_at"],
|
|
49
|
+
updated_at=row["updated_at"],
|
|
50
|
+
project_id=row["project_id"],
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
def to_dict(self) -> dict[str, Any]:
|
|
54
|
+
"""Convert to dictionary."""
|
|
55
|
+
return {
|
|
56
|
+
"id": self.id,
|
|
57
|
+
"name": self.name,
|
|
58
|
+
"project_id": self.project_id,
|
|
59
|
+
"transport": self.transport,
|
|
60
|
+
"url": self.url,
|
|
61
|
+
"command": self.command,
|
|
62
|
+
"args": self.args,
|
|
63
|
+
"env": self.env,
|
|
64
|
+
"headers": self.headers,
|
|
65
|
+
"enabled": self.enabled,
|
|
66
|
+
"description": self.description,
|
|
67
|
+
"created_at": self.created_at,
|
|
68
|
+
"updated_at": self.updated_at,
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
def to_config(self) -> dict[str, Any]:
|
|
72
|
+
"""Convert to MCP config format."""
|
|
73
|
+
config: dict[str, Any] = {
|
|
74
|
+
"name": self.name,
|
|
75
|
+
"transport": self.transport,
|
|
76
|
+
"enabled": self.enabled,
|
|
77
|
+
}
|
|
78
|
+
if self.project_id:
|
|
79
|
+
config["project_id"] = self.project_id
|
|
80
|
+
if self.url:
|
|
81
|
+
config["url"] = self.url
|
|
82
|
+
if self.command:
|
|
83
|
+
config["command"] = self.command
|
|
84
|
+
if self.args:
|
|
85
|
+
config["args"] = self.args
|
|
86
|
+
if self.env:
|
|
87
|
+
config["env"] = self.env
|
|
88
|
+
if self.headers:
|
|
89
|
+
config["headers"] = self.headers
|
|
90
|
+
if self.description:
|
|
91
|
+
config["description"] = self.description
|
|
92
|
+
return config
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@dataclass
|
|
96
|
+
class Tool:
|
|
97
|
+
"""MCP tool model."""
|
|
98
|
+
|
|
99
|
+
id: str
|
|
100
|
+
mcp_server_id: str
|
|
101
|
+
name: str
|
|
102
|
+
description: str | None
|
|
103
|
+
input_schema: dict[str, Any] | None
|
|
104
|
+
created_at: str
|
|
105
|
+
updated_at: str
|
|
106
|
+
|
|
107
|
+
@classmethod
|
|
108
|
+
def from_row(cls, row: Any) -> "Tool":
|
|
109
|
+
"""Create Tool from database row."""
|
|
110
|
+
return cls(
|
|
111
|
+
id=row["id"],
|
|
112
|
+
mcp_server_id=row["mcp_server_id"],
|
|
113
|
+
name=row["name"],
|
|
114
|
+
description=row["description"],
|
|
115
|
+
input_schema=json.loads(row["input_schema"]) if row["input_schema"] else None,
|
|
116
|
+
created_at=row["created_at"],
|
|
117
|
+
updated_at=row["updated_at"],
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
def to_dict(self) -> dict[str, Any]:
|
|
121
|
+
"""Convert to dictionary."""
|
|
122
|
+
return {
|
|
123
|
+
"id": self.id,
|
|
124
|
+
"mcp_server_id": self.mcp_server_id,
|
|
125
|
+
"name": self.name,
|
|
126
|
+
"description": self.description,
|
|
127
|
+
"input_schema": self.input_schema,
|
|
128
|
+
"created_at": self.created_at,
|
|
129
|
+
"updated_at": self.updated_at,
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class LocalMCPManager:
|
|
134
|
+
"""Manager for local MCP server and tool storage."""
|
|
135
|
+
|
|
136
|
+
def __init__(self, db: DatabaseProtocol):
|
|
137
|
+
"""Initialize with database connection."""
|
|
138
|
+
self.db = db
|
|
139
|
+
|
|
140
|
+
def upsert(
|
|
141
|
+
self,
|
|
142
|
+
name: str,
|
|
143
|
+
transport: str,
|
|
144
|
+
project_id: str,
|
|
145
|
+
url: str | None = None,
|
|
146
|
+
command: str | None = None,
|
|
147
|
+
args: list[str] | None = None,
|
|
148
|
+
env: dict[str, str] | None = None,
|
|
149
|
+
headers: dict[str, str] | None = None,
|
|
150
|
+
enabled: bool = True,
|
|
151
|
+
description: str | None = None,
|
|
152
|
+
) -> MCPServer:
|
|
153
|
+
"""
|
|
154
|
+
Insert or update an MCP server in the database.
|
|
155
|
+
|
|
156
|
+
Server name is normalized to lowercase.
|
|
157
|
+
Uniqueness is enforced on (name, project_id) - same name can exist
|
|
158
|
+
in different projects.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
name: Server name (normalized to lowercase)
|
|
162
|
+
transport: Transport type (http, stdio, websocket)
|
|
163
|
+
project_id: Required project ID - all servers must belong to a project
|
|
164
|
+
"""
|
|
165
|
+
# Normalize server name to lowercase
|
|
166
|
+
name = name.lower()
|
|
167
|
+
|
|
168
|
+
server_id = str(uuid.uuid4())
|
|
169
|
+
now = datetime.now(UTC).isoformat()
|
|
170
|
+
|
|
171
|
+
self.db.execute(
|
|
172
|
+
"""
|
|
173
|
+
INSERT INTO mcp_servers (
|
|
174
|
+
id, name, project_id, transport, url, command, args, env, headers,
|
|
175
|
+
enabled, description, created_at, updated_at
|
|
176
|
+
)
|
|
177
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
178
|
+
ON CONFLICT(name, project_id) DO UPDATE SET
|
|
179
|
+
transport = excluded.transport,
|
|
180
|
+
url = excluded.url,
|
|
181
|
+
command = excluded.command,
|
|
182
|
+
args = excluded.args,
|
|
183
|
+
env = excluded.env,
|
|
184
|
+
headers = excluded.headers,
|
|
185
|
+
enabled = excluded.enabled,
|
|
186
|
+
description = COALESCE(excluded.description, description),
|
|
187
|
+
updated_at = excluded.updated_at
|
|
188
|
+
""",
|
|
189
|
+
(
|
|
190
|
+
server_id,
|
|
191
|
+
name,
|
|
192
|
+
project_id,
|
|
193
|
+
transport,
|
|
194
|
+
url,
|
|
195
|
+
command,
|
|
196
|
+
json.dumps(args) if args else None,
|
|
197
|
+
json.dumps(env) if env else None,
|
|
198
|
+
json.dumps(headers) if headers else None,
|
|
199
|
+
1 if enabled else 0,
|
|
200
|
+
description,
|
|
201
|
+
now,
|
|
202
|
+
now,
|
|
203
|
+
),
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
server = self.get_server(name, project_id=project_id)
|
|
207
|
+
if not server:
|
|
208
|
+
raise RuntimeError(f"Failed to retrieve server '{name}' after upsert")
|
|
209
|
+
return server
|
|
210
|
+
|
|
211
|
+
def get_server(self, name: str, project_id: str) -> MCPServer | None:
|
|
212
|
+
"""
|
|
213
|
+
Get server by name (case-insensitive lookup).
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
name: Server name
|
|
217
|
+
project_id: Required project ID
|
|
218
|
+
"""
|
|
219
|
+
# Normalize to lowercase for lookup
|
|
220
|
+
name = name.lower()
|
|
221
|
+
|
|
222
|
+
row = self.db.fetchone(
|
|
223
|
+
"SELECT * FROM mcp_servers WHERE name = ? AND project_id = ?",
|
|
224
|
+
(name, project_id),
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
return MCPServer.from_row(row) if row else None
|
|
228
|
+
|
|
229
|
+
def get_server_by_id(self, server_id: str) -> MCPServer | None:
|
|
230
|
+
"""Get server by ID."""
|
|
231
|
+
row = self.db.fetchone("SELECT * FROM mcp_servers WHERE id = ?", (server_id,))
|
|
232
|
+
return MCPServer.from_row(row) if row else None
|
|
233
|
+
|
|
234
|
+
def list_servers(
|
|
235
|
+
self,
|
|
236
|
+
project_id: str,
|
|
237
|
+
enabled_only: bool = True,
|
|
238
|
+
) -> list[MCPServer]:
|
|
239
|
+
"""
|
|
240
|
+
List MCP servers for a project.
|
|
241
|
+
|
|
242
|
+
Args:
|
|
243
|
+
project_id: Required project ID
|
|
244
|
+
enabled_only: Only return enabled servers
|
|
245
|
+
|
|
246
|
+
Returns:
|
|
247
|
+
List of servers for the project.
|
|
248
|
+
"""
|
|
249
|
+
conditions = ["project_id = ?"]
|
|
250
|
+
params: list[Any] = [project_id]
|
|
251
|
+
|
|
252
|
+
if enabled_only:
|
|
253
|
+
conditions.append("enabled = 1")
|
|
254
|
+
|
|
255
|
+
# nosec B608: where_clause built from hardcoded condition strings, values parameterized
|
|
256
|
+
where_clause = " AND ".join(conditions)
|
|
257
|
+
query = f"SELECT * FROM mcp_servers WHERE {where_clause} ORDER BY name" # nosec B608
|
|
258
|
+
rows = self.db.fetchall(query, tuple(params))
|
|
259
|
+
|
|
260
|
+
return [MCPServer.from_row(row) for row in rows]
|
|
261
|
+
|
|
262
|
+
def list_all_servers(self, enabled_only: bool = True) -> list[MCPServer]:
|
|
263
|
+
"""
|
|
264
|
+
List all MCP servers across all projects.
|
|
265
|
+
|
|
266
|
+
Used by the daemon to load all servers on startup.
|
|
267
|
+
|
|
268
|
+
Args:
|
|
269
|
+
enabled_only: Only return enabled servers
|
|
270
|
+
|
|
271
|
+
Returns:
|
|
272
|
+
List of all servers.
|
|
273
|
+
"""
|
|
274
|
+
if enabled_only:
|
|
275
|
+
query = "SELECT * FROM mcp_servers WHERE enabled = 1 ORDER BY name"
|
|
276
|
+
else:
|
|
277
|
+
query = "SELECT * FROM mcp_servers ORDER BY name"
|
|
278
|
+
rows = self.db.fetchall(query, ())
|
|
279
|
+
return [MCPServer.from_row(row) for row in rows]
|
|
280
|
+
|
|
281
|
+
def update_server(self, name: str, project_id: str, **fields: Any) -> MCPServer | None:
|
|
282
|
+
"""
|
|
283
|
+
Update server fields.
|
|
284
|
+
|
|
285
|
+
Args:
|
|
286
|
+
name: Server name
|
|
287
|
+
project_id: Required project ID
|
|
288
|
+
"""
|
|
289
|
+
server = self.get_server(name, project_id=project_id)
|
|
290
|
+
if not server:
|
|
291
|
+
return None
|
|
292
|
+
|
|
293
|
+
allowed = {
|
|
294
|
+
"transport",
|
|
295
|
+
"url",
|
|
296
|
+
"command",
|
|
297
|
+
"args",
|
|
298
|
+
"env",
|
|
299
|
+
"headers",
|
|
300
|
+
"enabled",
|
|
301
|
+
"description",
|
|
302
|
+
}
|
|
303
|
+
fields = {k: v for k, v in fields.items() if k in allowed}
|
|
304
|
+
if not fields:
|
|
305
|
+
return server
|
|
306
|
+
|
|
307
|
+
# Serialize JSON fields
|
|
308
|
+
if "args" in fields and fields["args"] is not None:
|
|
309
|
+
fields["args"] = json.dumps(fields["args"])
|
|
310
|
+
if "env" in fields and fields["env"] is not None:
|
|
311
|
+
fields["env"] = json.dumps(fields["env"])
|
|
312
|
+
if "headers" in fields and fields["headers"] is not None:
|
|
313
|
+
fields["headers"] = json.dumps(fields["headers"])
|
|
314
|
+
if "enabled" in fields:
|
|
315
|
+
fields["enabled"] = 1 if fields["enabled"] else 0
|
|
316
|
+
|
|
317
|
+
fields["updated_at"] = datetime.now(UTC).isoformat()
|
|
318
|
+
|
|
319
|
+
# nosec B608: Fields validated against allowlist above, values parameterized
|
|
320
|
+
set_clause = ", ".join(f"{k} = ?" for k in fields)
|
|
321
|
+
# Update by server ID to be precise
|
|
322
|
+
values = list(fields.values()) + [server.id]
|
|
323
|
+
|
|
324
|
+
self.db.execute(
|
|
325
|
+
f"UPDATE mcp_servers SET {set_clause} WHERE id = ?", # nosec B608
|
|
326
|
+
tuple(values),
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
return self.get_server(name, project_id=project_id)
|
|
330
|
+
|
|
331
|
+
def remove_server(self, name: str, project_id: str) -> bool:
|
|
332
|
+
"""
|
|
333
|
+
Remove server by name (cascades to tools). Case-insensitive.
|
|
334
|
+
|
|
335
|
+
Args:
|
|
336
|
+
name: Server name
|
|
337
|
+
project_id: Required project ID
|
|
338
|
+
"""
|
|
339
|
+
name = name.lower()
|
|
340
|
+
cursor = self.db.execute(
|
|
341
|
+
"DELETE FROM mcp_servers WHERE name = ? AND project_id = ?",
|
|
342
|
+
(name, project_id),
|
|
343
|
+
)
|
|
344
|
+
return cursor.rowcount > 0
|
|
345
|
+
|
|
346
|
+
def cache_tools(self, server_name: str, tools: list[dict[str, Any]], project_id: str) -> int:
|
|
347
|
+
"""
|
|
348
|
+
Cache tools for a server.
|
|
349
|
+
|
|
350
|
+
Replaces existing tools for the server.
|
|
351
|
+
|
|
352
|
+
Args:
|
|
353
|
+
server_name: Server name
|
|
354
|
+
tools: List of tool definitions with name, description, and inputSchema (or args)
|
|
355
|
+
project_id: Required project ID
|
|
356
|
+
|
|
357
|
+
Returns:
|
|
358
|
+
Number of tools cached
|
|
359
|
+
"""
|
|
360
|
+
server = self.get_server(server_name, project_id=project_id)
|
|
361
|
+
if not server:
|
|
362
|
+
logger.warning(f"Server not found: {server_name}")
|
|
363
|
+
return 0
|
|
364
|
+
|
|
365
|
+
# Delete existing tools
|
|
366
|
+
self.db.execute("DELETE FROM tools WHERE mcp_server_id = ?", (server.id,))
|
|
367
|
+
|
|
368
|
+
# Insert new tools
|
|
369
|
+
now = datetime.now(UTC).isoformat()
|
|
370
|
+
for tool in tools:
|
|
371
|
+
tool_id = str(uuid.uuid4())
|
|
372
|
+
# Handle both 'inputSchema' and 'args' keys (internal vs MCP standard)
|
|
373
|
+
input_schema = tool.get("inputSchema") or tool.get("args")
|
|
374
|
+
# Normalize tool name to lowercase
|
|
375
|
+
tool_name = (tool.get("name") or "").lower()
|
|
376
|
+
self.db.execute(
|
|
377
|
+
"""
|
|
378
|
+
INSERT INTO tools (id, mcp_server_id, name, description, input_schema, created_at, updated_at)
|
|
379
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
380
|
+
""",
|
|
381
|
+
(
|
|
382
|
+
tool_id,
|
|
383
|
+
server.id,
|
|
384
|
+
tool_name,
|
|
385
|
+
tool.get("description"),
|
|
386
|
+
json.dumps(input_schema) if input_schema else None,
|
|
387
|
+
now,
|
|
388
|
+
now,
|
|
389
|
+
),
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
return len(tools)
|
|
393
|
+
|
|
394
|
+
def get_cached_tools(self, server_name: str, project_id: str) -> list[Tool]:
|
|
395
|
+
"""
|
|
396
|
+
Get cached tools for a server.
|
|
397
|
+
|
|
398
|
+
Args:
|
|
399
|
+
server_name: Server name
|
|
400
|
+
project_id: Required project ID
|
|
401
|
+
"""
|
|
402
|
+
server = self.get_server(server_name, project_id=project_id)
|
|
403
|
+
if not server:
|
|
404
|
+
return []
|
|
405
|
+
|
|
406
|
+
rows = self.db.fetchall(
|
|
407
|
+
"SELECT * FROM tools WHERE mcp_server_id = ? ORDER BY name",
|
|
408
|
+
(server.id,),
|
|
409
|
+
)
|
|
410
|
+
return [Tool.from_row(row) for row in rows]
|
|
411
|
+
|
|
412
|
+
def refresh_tools_incremental(
|
|
413
|
+
self,
|
|
414
|
+
server_name: str,
|
|
415
|
+
tools: list[dict[str, Any]],
|
|
416
|
+
project_id: str,
|
|
417
|
+
schema_hash_manager: Any | None = None,
|
|
418
|
+
) -> dict[str, Any]:
|
|
419
|
+
"""
|
|
420
|
+
Incrementally refresh tools for a server.
|
|
421
|
+
|
|
422
|
+
Only updates tools that have changed based on schema hash comparison.
|
|
423
|
+
New tools are added, changed tools are updated, removed tools are deleted.
|
|
424
|
+
|
|
425
|
+
Args:
|
|
426
|
+
server_name: Server name
|
|
427
|
+
tools: List of current tool definitions from the server
|
|
428
|
+
project_id: Required project ID
|
|
429
|
+
schema_hash_manager: Optional SchemaHashManager for change detection.
|
|
430
|
+
If not provided, falls back to full cache_tools() behavior.
|
|
431
|
+
|
|
432
|
+
Returns:
|
|
433
|
+
Dict with refresh statistics:
|
|
434
|
+
- added: number of new tools added
|
|
435
|
+
- updated: number of changed tools updated
|
|
436
|
+
- removed: number of stale tools removed
|
|
437
|
+
- unchanged: number of unchanged tools skipped
|
|
438
|
+
- total: total tools after refresh
|
|
439
|
+
"""
|
|
440
|
+
from gobby.mcp_proxy.schema_hash import compute_schema_hash
|
|
441
|
+
|
|
442
|
+
server = self.get_server(server_name, project_id=project_id)
|
|
443
|
+
if not server:
|
|
444
|
+
logger.warning(f"Server not found: {server_name}")
|
|
445
|
+
return {"added": 0, "updated": 0, "removed": 0, "unchanged": 0, "total": 0}
|
|
446
|
+
|
|
447
|
+
stats = {"added": 0, "updated": 0, "removed": 0, "unchanged": 0}
|
|
448
|
+
now = datetime.now(UTC).isoformat()
|
|
449
|
+
|
|
450
|
+
# Build map of current tools by name
|
|
451
|
+
current_tool_names = set()
|
|
452
|
+
for tool in tools:
|
|
453
|
+
tool_name = (tool.get("name") or "").lower()
|
|
454
|
+
current_tool_names.add(tool_name)
|
|
455
|
+
|
|
456
|
+
# Get existing tools
|
|
457
|
+
existing_tools = {t.name: t for t in self.get_cached_tools(server_name, project_id)}
|
|
458
|
+
|
|
459
|
+
# Detect changes using schema hash if manager available
|
|
460
|
+
if schema_hash_manager:
|
|
461
|
+
changes = schema_hash_manager.check_tools_for_changes(server_name, project_id, tools)
|
|
462
|
+
new_tools = set(changes["new"])
|
|
463
|
+
changed_tools = set(changes["changed"])
|
|
464
|
+
else:
|
|
465
|
+
# Without hash manager, treat all as potentially changed
|
|
466
|
+
new_tools = current_tool_names - set(existing_tools.keys())
|
|
467
|
+
changed_tools = current_tool_names & set(existing_tools.keys())
|
|
468
|
+
|
|
469
|
+
# Process each tool
|
|
470
|
+
for tool in tools:
|
|
471
|
+
tool_name = (tool.get("name") or "").lower()
|
|
472
|
+
input_schema = tool.get("inputSchema") or tool.get("args")
|
|
473
|
+
|
|
474
|
+
if tool_name in new_tools:
|
|
475
|
+
# Add new tool
|
|
476
|
+
tool_id = str(uuid.uuid4())
|
|
477
|
+
self.db.execute(
|
|
478
|
+
"""
|
|
479
|
+
INSERT INTO tools (id, mcp_server_id, name, description, input_schema, created_at, updated_at)
|
|
480
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
481
|
+
""",
|
|
482
|
+
(
|
|
483
|
+
tool_id,
|
|
484
|
+
server.id,
|
|
485
|
+
tool_name,
|
|
486
|
+
tool.get("description"),
|
|
487
|
+
json.dumps(input_schema) if input_schema else None,
|
|
488
|
+
now,
|
|
489
|
+
now,
|
|
490
|
+
),
|
|
491
|
+
)
|
|
492
|
+
stats["added"] += 1
|
|
493
|
+
|
|
494
|
+
# Store hash for new tool
|
|
495
|
+
if schema_hash_manager:
|
|
496
|
+
schema_hash = compute_schema_hash(input_schema)
|
|
497
|
+
schema_hash_manager.store_hash(server_name, tool_name, project_id, schema_hash)
|
|
498
|
+
|
|
499
|
+
elif tool_name in changed_tools:
|
|
500
|
+
# Update changed tool
|
|
501
|
+
existing = existing_tools[tool_name]
|
|
502
|
+
self.db.execute(
|
|
503
|
+
"""
|
|
504
|
+
UPDATE tools
|
|
505
|
+
SET description = ?, input_schema = ?, updated_at = ?
|
|
506
|
+
WHERE id = ?
|
|
507
|
+
""",
|
|
508
|
+
(
|
|
509
|
+
tool.get("description"),
|
|
510
|
+
json.dumps(input_schema) if input_schema else None,
|
|
511
|
+
now,
|
|
512
|
+
existing.id,
|
|
513
|
+
),
|
|
514
|
+
)
|
|
515
|
+
stats["updated"] += 1
|
|
516
|
+
|
|
517
|
+
# Update hash for changed tool
|
|
518
|
+
if schema_hash_manager:
|
|
519
|
+
schema_hash = compute_schema_hash(input_schema)
|
|
520
|
+
schema_hash_manager.store_hash(server_name, tool_name, project_id, schema_hash)
|
|
521
|
+
|
|
522
|
+
else:
|
|
523
|
+
# Unchanged tool - just update verification time
|
|
524
|
+
stats["unchanged"] += 1
|
|
525
|
+
if schema_hash_manager:
|
|
526
|
+
schema_hash_manager.update_verification_time(server_name, tool_name, project_id)
|
|
527
|
+
|
|
528
|
+
# Remove stale tools (tools that no longer exist on server)
|
|
529
|
+
stale_tools = set(existing_tools.keys()) - current_tool_names
|
|
530
|
+
for tool_name in stale_tools:
|
|
531
|
+
existing = existing_tools[tool_name]
|
|
532
|
+
self.db.execute("DELETE FROM tools WHERE id = ?", (existing.id,))
|
|
533
|
+
stats["removed"] += 1
|
|
534
|
+
|
|
535
|
+
# Cleanup stale hashes
|
|
536
|
+
if schema_hash_manager:
|
|
537
|
+
schema_hash_manager.cleanup_stale_hashes(
|
|
538
|
+
server_name, project_id, list(current_tool_names)
|
|
539
|
+
)
|
|
540
|
+
|
|
541
|
+
stats["total"] = len(tools)
|
|
542
|
+
logger.debug(
|
|
543
|
+
f"Incremental refresh for {server_name}: "
|
|
544
|
+
f"+{stats['added']} ~{stats['updated']} -{stats['removed']} ={stats['unchanged']}"
|
|
545
|
+
)
|
|
546
|
+
return stats
|
|
547
|
+
|
|
548
|
+
def import_from_mcp_json(self, path: str | Path, project_id: str) -> int:
|
|
549
|
+
"""
|
|
550
|
+
Import servers from .mcp.json file.
|
|
551
|
+
|
|
552
|
+
Supports both formats:
|
|
553
|
+
- Claude Code format: {"mcpServers": {"server_name": {...}, ...}}
|
|
554
|
+
- Gobby format: {"servers": [{"name": "server_name", ...}, ...]}
|
|
555
|
+
|
|
556
|
+
Args:
|
|
557
|
+
path: Path to .mcp.json file
|
|
558
|
+
project_id: Required project ID
|
|
559
|
+
|
|
560
|
+
Returns:
|
|
561
|
+
Number of servers imported
|
|
562
|
+
"""
|
|
563
|
+
path = Path(path)
|
|
564
|
+
if not path.exists():
|
|
565
|
+
return 0
|
|
566
|
+
|
|
567
|
+
try:
|
|
568
|
+
with open(path) as f:
|
|
569
|
+
data = json.load(f)
|
|
570
|
+
except (json.JSONDecodeError, OSError) as e:
|
|
571
|
+
logger.error(f"Failed to read {path}: {e}")
|
|
572
|
+
return 0
|
|
573
|
+
|
|
574
|
+
imported = 0
|
|
575
|
+
|
|
576
|
+
# Handle Gobby format: {"servers": [{"name": "...", ...}, ...]}
|
|
577
|
+
if "servers" in data and isinstance(data["servers"], list):
|
|
578
|
+
for config in data["servers"]:
|
|
579
|
+
name = config.get("name")
|
|
580
|
+
if not name:
|
|
581
|
+
continue
|
|
582
|
+
|
|
583
|
+
transport = config.get("transport", "stdio")
|
|
584
|
+
self.upsert(
|
|
585
|
+
name=name,
|
|
586
|
+
transport=transport,
|
|
587
|
+
url=config.get("url"),
|
|
588
|
+
command=config.get("command"),
|
|
589
|
+
args=config.get("args"),
|
|
590
|
+
env=config.get("env"),
|
|
591
|
+
headers=config.get("headers"),
|
|
592
|
+
enabled=config.get("enabled", True),
|
|
593
|
+
description=config.get("description"),
|
|
594
|
+
project_id=project_id,
|
|
595
|
+
)
|
|
596
|
+
imported += 1
|
|
597
|
+
|
|
598
|
+
# Handle Claude Code format: {"mcpServers": {"server_name": {...}, ...}}
|
|
599
|
+
elif "mcpServers" in data and isinstance(data["mcpServers"], dict):
|
|
600
|
+
for name, config in data["mcpServers"].items():
|
|
601
|
+
transport = config.get("transport", "stdio")
|
|
602
|
+
self.upsert(
|
|
603
|
+
name=name,
|
|
604
|
+
transport=transport,
|
|
605
|
+
url=config.get("url"),
|
|
606
|
+
command=config.get("command"),
|
|
607
|
+
args=config.get("args"),
|
|
608
|
+
env=config.get("env"),
|
|
609
|
+
headers=config.get("headers"),
|
|
610
|
+
enabled=config.get("enabled", True),
|
|
611
|
+
description=config.get("description"),
|
|
612
|
+
project_id=project_id,
|
|
613
|
+
)
|
|
614
|
+
imported += 1
|
|
615
|
+
|
|
616
|
+
return imported
|
|
617
|
+
|
|
618
|
+
def import_tools_from_filesystem(
|
|
619
|
+
self, project_id: str, tools_dir: str | Path | None = None
|
|
620
|
+
) -> int:
|
|
621
|
+
"""
|
|
622
|
+
Import tool schemas from filesystem directory.
|
|
623
|
+
|
|
624
|
+
Reads tool JSON files from ~/.gobby/tools/<server_name>/<tool_name>.json
|
|
625
|
+
and caches them in the database for servers that exist in the project.
|
|
626
|
+
|
|
627
|
+
Args:
|
|
628
|
+
project_id: Required project ID
|
|
629
|
+
tools_dir: Path to tools directory (default: ~/.gobby/tools)
|
|
630
|
+
|
|
631
|
+
Returns:
|
|
632
|
+
Number of tools imported
|
|
633
|
+
"""
|
|
634
|
+
if tools_dir is None:
|
|
635
|
+
tools_dir = Path.home() / ".gobby" / "tools"
|
|
636
|
+
else:
|
|
637
|
+
tools_dir = Path(tools_dir)
|
|
638
|
+
|
|
639
|
+
if not tools_dir.exists():
|
|
640
|
+
return 0
|
|
641
|
+
|
|
642
|
+
total_imported = 0
|
|
643
|
+
|
|
644
|
+
# Iterate through server directories
|
|
645
|
+
for server_dir in tools_dir.iterdir():
|
|
646
|
+
if not server_dir.is_dir() or server_dir.name.startswith("."):
|
|
647
|
+
continue
|
|
648
|
+
|
|
649
|
+
server_name = server_dir.name
|
|
650
|
+
|
|
651
|
+
# Check if server exists in database for this project
|
|
652
|
+
server = self.get_server(server_name, project_id=project_id)
|
|
653
|
+
if not server:
|
|
654
|
+
logger.debug(f"Skipping tools for unknown server: {server_name}")
|
|
655
|
+
continue
|
|
656
|
+
|
|
657
|
+
# Collect all tool schemas for this server
|
|
658
|
+
tools = []
|
|
659
|
+
for tool_file in server_dir.glob("*.json"):
|
|
660
|
+
try:
|
|
661
|
+
with open(tool_file) as f:
|
|
662
|
+
tool_data = json.load(f)
|
|
663
|
+
tools.append(
|
|
664
|
+
{
|
|
665
|
+
"name": tool_data.get("name", tool_file.stem),
|
|
666
|
+
"description": tool_data.get("description"),
|
|
667
|
+
"inputSchema": tool_data.get("inputSchema", {}),
|
|
668
|
+
}
|
|
669
|
+
)
|
|
670
|
+
except (json.JSONDecodeError, OSError) as e:
|
|
671
|
+
logger.warning(f"Failed to read tool file {tool_file}: {e}")
|
|
672
|
+
continue
|
|
673
|
+
|
|
674
|
+
# Cache tools to database
|
|
675
|
+
if tools:
|
|
676
|
+
count = self.cache_tools(server_name, tools, project_id=project_id)
|
|
677
|
+
total_imported += count
|
|
678
|
+
logger.info(f"Imported {count} tools for server '{server_name}'")
|
|
679
|
+
|
|
680
|
+
return total_imported
|