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/cli/mcp_proxy.py
ADDED
|
@@ -0,0 +1,698 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MCP proxy CLI commands.
|
|
3
|
+
|
|
4
|
+
Provides CLI access to MCP proxy functionality:
|
|
5
|
+
- list-servers: List configured MCP servers
|
|
6
|
+
- list-tools: List tools from MCP servers
|
|
7
|
+
- get-schema: Get full schema for a specific tool
|
|
8
|
+
- call-tool: Execute a tool on an MCP server
|
|
9
|
+
- add-server: Add a new MCP server configuration
|
|
10
|
+
- remove-server: Remove an MCP server configuration
|
|
11
|
+
- recommend-tools: Get AI-powered tool recommendations
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
import sys
|
|
16
|
+
import urllib.parse
|
|
17
|
+
from typing import Any, cast
|
|
18
|
+
|
|
19
|
+
import click
|
|
20
|
+
|
|
21
|
+
from gobby.config.app import DaemonConfig
|
|
22
|
+
from gobby.utils.daemon_client import DaemonClient
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def get_daemon_client(ctx: click.Context) -> DaemonClient:
|
|
26
|
+
"""Get daemon client from context config."""
|
|
27
|
+
config: DaemonConfig = ctx.obj["config"]
|
|
28
|
+
return DaemonClient(host="localhost", port=config.daemon_port)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def check_daemon_running(client: DaemonClient) -> bool:
|
|
32
|
+
"""Check if daemon is running and print error if not."""
|
|
33
|
+
is_healthy, error = client.check_health()
|
|
34
|
+
if not is_healthy:
|
|
35
|
+
if error is None:
|
|
36
|
+
click.echo("Error: Gobby daemon is not running. Start it with: gobby start", err=True)
|
|
37
|
+
else:
|
|
38
|
+
click.echo(f"Error: Cannot connect to daemon: {error}", err=True)
|
|
39
|
+
return False
|
|
40
|
+
return True
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def call_mcp_api(
|
|
44
|
+
client: DaemonClient,
|
|
45
|
+
endpoint: str,
|
|
46
|
+
method: str = "POST",
|
|
47
|
+
json_data: dict[str, Any] | None = None,
|
|
48
|
+
timeout: float = 30.0,
|
|
49
|
+
) -> dict[str, Any] | None:
|
|
50
|
+
"""Call MCP API endpoint and handle errors."""
|
|
51
|
+
try:
|
|
52
|
+
response = client.call_http_api(
|
|
53
|
+
endpoint, method=method, json_data=json_data, timeout=timeout
|
|
54
|
+
)
|
|
55
|
+
if response.status_code == 200:
|
|
56
|
+
return cast(dict[str, Any], response.json())
|
|
57
|
+
else:
|
|
58
|
+
error_msg = response.text or f"HTTP {response.status_code}"
|
|
59
|
+
click.echo(f"Error: {error_msg}", err=True)
|
|
60
|
+
return None
|
|
61
|
+
except Exception as e:
|
|
62
|
+
click.echo(f"Error: {e}", err=True)
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@click.group("mcp-proxy")
|
|
67
|
+
def mcp_proxy() -> None:
|
|
68
|
+
"""Manage MCP proxy servers and tools."""
|
|
69
|
+
pass
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@mcp_proxy.command("list-servers")
|
|
73
|
+
@click.option("--json", "json_format", is_flag=True, help="Output as JSON")
|
|
74
|
+
@click.pass_context
|
|
75
|
+
def list_servers(ctx: click.Context, json_format: bool) -> None:
|
|
76
|
+
"""List all configured MCP servers."""
|
|
77
|
+
client = get_daemon_client(ctx)
|
|
78
|
+
if not check_daemon_running(client):
|
|
79
|
+
sys.exit(1)
|
|
80
|
+
|
|
81
|
+
result = call_mcp_api(client, "/mcp/servers", method="GET")
|
|
82
|
+
if result is None:
|
|
83
|
+
sys.exit(1)
|
|
84
|
+
|
|
85
|
+
servers = result.get("servers", [])
|
|
86
|
+
|
|
87
|
+
if json_format:
|
|
88
|
+
click.echo(json.dumps(result, indent=2))
|
|
89
|
+
return
|
|
90
|
+
|
|
91
|
+
if not servers:
|
|
92
|
+
click.echo("No MCP servers configured.")
|
|
93
|
+
return
|
|
94
|
+
|
|
95
|
+
connected = result.get("connected_count", 0)
|
|
96
|
+
total = result.get("total_count", 0)
|
|
97
|
+
click.echo(f"MCP Servers ({connected}/{total} connected):")
|
|
98
|
+
for server in servers:
|
|
99
|
+
status_icon = "●" if server.get("connected") else "○"
|
|
100
|
+
state = server.get("state", "unknown")
|
|
101
|
+
click.echo(f" {status_icon} {server['name']} ({state})")
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@mcp_proxy.command("list-tools")
|
|
105
|
+
@click.option("--server", "-s", help="Filter by server name")
|
|
106
|
+
@click.option("--json", "json_format", is_flag=True, help="Output as JSON")
|
|
107
|
+
@click.pass_context
|
|
108
|
+
def list_tools(ctx: click.Context, server: str | None, json_format: bool) -> None:
|
|
109
|
+
"""List tools from MCP servers."""
|
|
110
|
+
client = get_daemon_client(ctx)
|
|
111
|
+
if not check_daemon_running(client):
|
|
112
|
+
sys.exit(1)
|
|
113
|
+
|
|
114
|
+
endpoint = "/mcp/tools"
|
|
115
|
+
if server:
|
|
116
|
+
encoded_server = urllib.parse.quote(server)
|
|
117
|
+
endpoint = f"/mcp/tools?server_filter={encoded_server}"
|
|
118
|
+
|
|
119
|
+
result = call_mcp_api(client, endpoint, method="GET")
|
|
120
|
+
if result is None:
|
|
121
|
+
sys.exit(1)
|
|
122
|
+
|
|
123
|
+
if json_format:
|
|
124
|
+
click.echo(json.dumps(result, indent=2))
|
|
125
|
+
return
|
|
126
|
+
|
|
127
|
+
tools_by_server = result.get("tools", {})
|
|
128
|
+
if not tools_by_server:
|
|
129
|
+
click.echo("No tools available.")
|
|
130
|
+
return
|
|
131
|
+
|
|
132
|
+
for server_name, tools in tools_by_server.items():
|
|
133
|
+
click.echo(f"\n{server_name}:")
|
|
134
|
+
if not tools:
|
|
135
|
+
click.echo(" (no tools)")
|
|
136
|
+
continue
|
|
137
|
+
for tool in tools:
|
|
138
|
+
name = tool.get("name", "unknown")
|
|
139
|
+
brief = tool.get("brief", tool.get("description", ""))[:60]
|
|
140
|
+
click.echo(f" • {name}")
|
|
141
|
+
if brief:
|
|
142
|
+
click.echo(f" {brief}")
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
@mcp_proxy.command("get-schema")
|
|
146
|
+
@click.argument("server_name")
|
|
147
|
+
@click.argument("tool_name")
|
|
148
|
+
@click.pass_context
|
|
149
|
+
def get_schema(ctx: click.Context, server_name: str, tool_name: str) -> None:
|
|
150
|
+
"""Get full schema for a specific tool.
|
|
151
|
+
|
|
152
|
+
Examples:
|
|
153
|
+
gobby mcp-proxy get-schema context7 get-library-docs
|
|
154
|
+
gobby mcp-proxy get-schema supabase list_tables
|
|
155
|
+
"""
|
|
156
|
+
client = get_daemon_client(ctx)
|
|
157
|
+
if not check_daemon_running(client):
|
|
158
|
+
sys.exit(1)
|
|
159
|
+
|
|
160
|
+
result = call_mcp_api(
|
|
161
|
+
client,
|
|
162
|
+
"/mcp/tools/schema",
|
|
163
|
+
method="POST",
|
|
164
|
+
json_data={"server_name": server_name, "tool_name": tool_name},
|
|
165
|
+
)
|
|
166
|
+
if result is None:
|
|
167
|
+
sys.exit(1)
|
|
168
|
+
|
|
169
|
+
# Always output as JSON for schema (it's complex)
|
|
170
|
+
click.echo(json.dumps(result, indent=2))
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
@mcp_proxy.command("call-tool")
|
|
174
|
+
@click.argument("server_name")
|
|
175
|
+
@click.argument("tool_name")
|
|
176
|
+
@click.option("--arg", "-a", "args", multiple=True, help="Tool argument in key=value format")
|
|
177
|
+
@click.option("--json-args", "-j", "json_args", help="Tool arguments as JSON string")
|
|
178
|
+
@click.option("--raw", is_flag=True, help="Output raw result without formatting")
|
|
179
|
+
@click.pass_context
|
|
180
|
+
def call_tool(
|
|
181
|
+
ctx: click.Context,
|
|
182
|
+
server_name: str,
|
|
183
|
+
tool_name: str,
|
|
184
|
+
args: tuple[str, ...],
|
|
185
|
+
json_args: str | None,
|
|
186
|
+
raw: bool,
|
|
187
|
+
) -> None:
|
|
188
|
+
"""Execute a tool on an MCP server.
|
|
189
|
+
|
|
190
|
+
Examples:
|
|
191
|
+
gobby mcp-proxy call-tool supabase list_tables
|
|
192
|
+
gobby mcp-proxy call-tool context7 get-library-docs -a topic=react -a tokens=5000
|
|
193
|
+
gobby mcp-proxy call-tool myserver mytool -j '{"key": "value"}'
|
|
194
|
+
"""
|
|
195
|
+
client = get_daemon_client(ctx)
|
|
196
|
+
if not check_daemon_running(client):
|
|
197
|
+
sys.exit(1)
|
|
198
|
+
|
|
199
|
+
# Parse arguments
|
|
200
|
+
arguments: dict[str, Any] = {}
|
|
201
|
+
|
|
202
|
+
if json_args:
|
|
203
|
+
try:
|
|
204
|
+
arguments = json.loads(json_args)
|
|
205
|
+
except json.JSONDecodeError as e:
|
|
206
|
+
click.echo(f"Error: Invalid JSON arguments: {e}", err=True)
|
|
207
|
+
sys.exit(1)
|
|
208
|
+
|
|
209
|
+
# Add key=value args (override JSON args)
|
|
210
|
+
for arg in args:
|
|
211
|
+
if "=" not in arg:
|
|
212
|
+
click.echo(f"Error: Invalid argument format '{arg}'. Use key=value", err=True)
|
|
213
|
+
sys.exit(1)
|
|
214
|
+
key, value = arg.split("=", 1)
|
|
215
|
+
# Try to parse value as JSON for proper typing
|
|
216
|
+
try:
|
|
217
|
+
arguments[key] = json.loads(value)
|
|
218
|
+
except json.JSONDecodeError:
|
|
219
|
+
arguments[key] = value
|
|
220
|
+
|
|
221
|
+
result = call_mcp_api(
|
|
222
|
+
client,
|
|
223
|
+
"/mcp/tools/call",
|
|
224
|
+
method="POST",
|
|
225
|
+
json_data={
|
|
226
|
+
"server_name": server_name,
|
|
227
|
+
"tool_name": tool_name,
|
|
228
|
+
"arguments": arguments,
|
|
229
|
+
},
|
|
230
|
+
)
|
|
231
|
+
if result is None:
|
|
232
|
+
sys.exit(1)
|
|
233
|
+
|
|
234
|
+
if raw:
|
|
235
|
+
click.echo(json.dumps(result, indent=2))
|
|
236
|
+
else:
|
|
237
|
+
# Format result nicely
|
|
238
|
+
if result.get("success"):
|
|
239
|
+
content = result.get("result", result)
|
|
240
|
+
if isinstance(content, dict):
|
|
241
|
+
click.echo(json.dumps(content, indent=2))
|
|
242
|
+
else:
|
|
243
|
+
click.echo(content)
|
|
244
|
+
else:
|
|
245
|
+
click.echo(f"Error: {result.get('error', 'Unknown error')}", err=True)
|
|
246
|
+
sys.exit(1)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
@mcp_proxy.command("add-server")
|
|
250
|
+
@click.argument("name")
|
|
251
|
+
@click.option("--transport", "-t", required=True, type=click.Choice(["http", "stdio", "websocket"]))
|
|
252
|
+
@click.option("--url", "-u", help="Server URL (for http/websocket)")
|
|
253
|
+
@click.option("--command", "-c", help="Command to run (for stdio)")
|
|
254
|
+
@click.option("--args", "-A", "cmd_args", help="Command arguments as JSON array (for stdio)")
|
|
255
|
+
@click.option("--env", "-e", help="Environment variables as JSON object")
|
|
256
|
+
@click.option("--headers", help="HTTP headers as JSON object")
|
|
257
|
+
@click.option("--disabled", is_flag=True, help="Add server as disabled")
|
|
258
|
+
@click.pass_context
|
|
259
|
+
def add_server(
|
|
260
|
+
ctx: click.Context,
|
|
261
|
+
name: str,
|
|
262
|
+
transport: str,
|
|
263
|
+
url: str | None,
|
|
264
|
+
command: str | None,
|
|
265
|
+
cmd_args: str | None,
|
|
266
|
+
env: str | None,
|
|
267
|
+
headers: str | None,
|
|
268
|
+
disabled: bool,
|
|
269
|
+
) -> None:
|
|
270
|
+
"""Add a new MCP server configuration.
|
|
271
|
+
|
|
272
|
+
Examples:
|
|
273
|
+
gobby mcp-proxy add-server my-http -t http -u https://api.example.com/mcp
|
|
274
|
+
gobby mcp-proxy add-server my-stdio -t stdio -c npx --args '["mcp-server"]'
|
|
275
|
+
"""
|
|
276
|
+
client = get_daemon_client(ctx)
|
|
277
|
+
if not check_daemon_running(client):
|
|
278
|
+
sys.exit(1)
|
|
279
|
+
|
|
280
|
+
# Validate transport requirements
|
|
281
|
+
if transport in ("http", "websocket") and not url:
|
|
282
|
+
click.echo(f"Error: --url is required for {transport} transport", err=True)
|
|
283
|
+
sys.exit(1)
|
|
284
|
+
if transport == "stdio" and not command:
|
|
285
|
+
click.echo("Error: --command is required for stdio transport", err=True)
|
|
286
|
+
sys.exit(1)
|
|
287
|
+
|
|
288
|
+
# Parse JSON options
|
|
289
|
+
parsed_args = None
|
|
290
|
+
parsed_env = None
|
|
291
|
+
parsed_headers = None
|
|
292
|
+
|
|
293
|
+
if cmd_args:
|
|
294
|
+
try:
|
|
295
|
+
parsed_args = json.loads(cmd_args)
|
|
296
|
+
except json.JSONDecodeError as e:
|
|
297
|
+
click.echo(f"Error: Invalid JSON for --args: {e}", err=True)
|
|
298
|
+
sys.exit(1)
|
|
299
|
+
|
|
300
|
+
if env:
|
|
301
|
+
try:
|
|
302
|
+
parsed_env = json.loads(env)
|
|
303
|
+
except json.JSONDecodeError as e:
|
|
304
|
+
click.echo(f"Error: Invalid JSON for --env: {e}", err=True)
|
|
305
|
+
sys.exit(1)
|
|
306
|
+
|
|
307
|
+
if headers:
|
|
308
|
+
try:
|
|
309
|
+
parsed_headers = json.loads(headers)
|
|
310
|
+
except json.JSONDecodeError as e:
|
|
311
|
+
click.echo(f"Error: Invalid JSON for --headers: {e}", err=True)
|
|
312
|
+
sys.exit(1)
|
|
313
|
+
|
|
314
|
+
result = call_mcp_api(
|
|
315
|
+
client,
|
|
316
|
+
"/mcp/servers",
|
|
317
|
+
method="POST",
|
|
318
|
+
json_data={
|
|
319
|
+
"name": name,
|
|
320
|
+
"transport": transport,
|
|
321
|
+
"url": url,
|
|
322
|
+
"command": command,
|
|
323
|
+
"args": parsed_args,
|
|
324
|
+
"env": parsed_env,
|
|
325
|
+
"headers": parsed_headers,
|
|
326
|
+
"enabled": not disabled,
|
|
327
|
+
},
|
|
328
|
+
)
|
|
329
|
+
if result is None:
|
|
330
|
+
sys.exit(1)
|
|
331
|
+
|
|
332
|
+
if result.get("success"):
|
|
333
|
+
click.echo(f"Added MCP server: {name}")
|
|
334
|
+
else:
|
|
335
|
+
click.echo(f"Error: {result.get('error', 'Failed to add server')}", err=True)
|
|
336
|
+
sys.exit(1)
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
@mcp_proxy.command("remove-server")
|
|
340
|
+
@click.argument("name")
|
|
341
|
+
@click.confirmation_option(prompt="Are you sure you want to remove this server?")
|
|
342
|
+
@click.pass_context
|
|
343
|
+
def remove_server(ctx: click.Context, name: str) -> None:
|
|
344
|
+
"""Remove an MCP server configuration."""
|
|
345
|
+
client = get_daemon_client(ctx)
|
|
346
|
+
if not check_daemon_running(client):
|
|
347
|
+
sys.exit(1)
|
|
348
|
+
|
|
349
|
+
encoded_name = urllib.parse.quote(name, safe="")
|
|
350
|
+
result = call_mcp_api(
|
|
351
|
+
client,
|
|
352
|
+
f"/mcp/servers/{encoded_name}",
|
|
353
|
+
method="DELETE",
|
|
354
|
+
)
|
|
355
|
+
if result is None:
|
|
356
|
+
sys.exit(1)
|
|
357
|
+
|
|
358
|
+
if result.get("success"):
|
|
359
|
+
click.echo(f"Removed MCP server: {name}")
|
|
360
|
+
else:
|
|
361
|
+
click.echo(f"Error: {result.get('error', 'Failed to remove server')}", err=True)
|
|
362
|
+
sys.exit(1)
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
@mcp_proxy.command("recommend-tools")
|
|
366
|
+
@click.argument("task_description")
|
|
367
|
+
@click.option("--agent", "-a", "agent_id", help="Agent ID for filtered recommendations")
|
|
368
|
+
@click.option(
|
|
369
|
+
"--mode",
|
|
370
|
+
"-m",
|
|
371
|
+
"search_mode",
|
|
372
|
+
type=click.Choice(["llm", "semantic", "hybrid"]),
|
|
373
|
+
default="llm",
|
|
374
|
+
help="Search mode: llm (default), semantic, or hybrid",
|
|
375
|
+
)
|
|
376
|
+
@click.option("--top-k", "-k", type=int, default=10, help="Max results (semantic/hybrid)")
|
|
377
|
+
@click.option("--json", "json_format", is_flag=True, help="Output as JSON")
|
|
378
|
+
@click.pass_context
|
|
379
|
+
def recommend_tools(
|
|
380
|
+
ctx: click.Context,
|
|
381
|
+
task_description: str,
|
|
382
|
+
agent_id: str | None,
|
|
383
|
+
search_mode: str,
|
|
384
|
+
top_k: int,
|
|
385
|
+
json_format: bool,
|
|
386
|
+
) -> None:
|
|
387
|
+
"""Get AI-powered tool recommendations for a task.
|
|
388
|
+
|
|
389
|
+
Examples:
|
|
390
|
+
gobby mcp-proxy recommend-tools "I need to query a database"
|
|
391
|
+
gobby mcp-proxy recommend-tools "Search for files" --mode semantic
|
|
392
|
+
gobby mcp-proxy recommend-tools "Search for documentation" --agent my-agent
|
|
393
|
+
"""
|
|
394
|
+
import os
|
|
395
|
+
|
|
396
|
+
client = get_daemon_client(ctx)
|
|
397
|
+
if not check_daemon_running(client):
|
|
398
|
+
sys.exit(1)
|
|
399
|
+
|
|
400
|
+
result = call_mcp_api(
|
|
401
|
+
client,
|
|
402
|
+
"/mcp/tools/recommend",
|
|
403
|
+
method="POST",
|
|
404
|
+
json_data={
|
|
405
|
+
"task_description": task_description,
|
|
406
|
+
"agent_id": agent_id,
|
|
407
|
+
"search_mode": search_mode,
|
|
408
|
+
"top_k": top_k,
|
|
409
|
+
"cwd": os.getcwd(),
|
|
410
|
+
},
|
|
411
|
+
timeout=120.0, # LLM/embedding generation can be slow
|
|
412
|
+
)
|
|
413
|
+
if result is None:
|
|
414
|
+
sys.exit(1)
|
|
415
|
+
|
|
416
|
+
if json_format:
|
|
417
|
+
click.echo(json.dumps(result, indent=2))
|
|
418
|
+
return
|
|
419
|
+
|
|
420
|
+
recommendations = result.get("recommendations", [])
|
|
421
|
+
if not recommendations:
|
|
422
|
+
click.echo("No tool recommendations found.")
|
|
423
|
+
return
|
|
424
|
+
|
|
425
|
+
click.echo("Recommended tools:")
|
|
426
|
+
for rec in recommendations:
|
|
427
|
+
server = rec.get("server", "unknown")
|
|
428
|
+
tool = rec.get("tool", "unknown")
|
|
429
|
+
reason = rec.get("reason", "")
|
|
430
|
+
click.echo(f" • {server}/{tool}")
|
|
431
|
+
if reason:
|
|
432
|
+
click.echo(f" {reason}")
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
@mcp_proxy.command("search-tools")
|
|
436
|
+
@click.argument("query")
|
|
437
|
+
@click.option("--top-k", "-k", type=int, default=10, help="Max results to return")
|
|
438
|
+
@click.option("--min-similarity", "-s", type=float, default=0.0, help="Min similarity threshold")
|
|
439
|
+
@click.option("--server", "-S", help="Filter by server name")
|
|
440
|
+
@click.option("--json", "json_format", is_flag=True, help="Output as JSON")
|
|
441
|
+
@click.pass_context
|
|
442
|
+
def search_tools(
|
|
443
|
+
ctx: click.Context,
|
|
444
|
+
query: str,
|
|
445
|
+
top_k: int,
|
|
446
|
+
min_similarity: float,
|
|
447
|
+
server: str | None,
|
|
448
|
+
json_format: bool,
|
|
449
|
+
) -> None:
|
|
450
|
+
"""Search for tools using semantic similarity.
|
|
451
|
+
|
|
452
|
+
Examples:
|
|
453
|
+
gobby mcp-proxy search-tools "query a database"
|
|
454
|
+
gobby mcp-proxy search-tools "search files" --top-k 5
|
|
455
|
+
"""
|
|
456
|
+
import os
|
|
457
|
+
|
|
458
|
+
client = get_daemon_client(ctx)
|
|
459
|
+
if not check_daemon_running(client):
|
|
460
|
+
sys.exit(1)
|
|
461
|
+
|
|
462
|
+
result = call_mcp_api(
|
|
463
|
+
client,
|
|
464
|
+
"/mcp/tools/search",
|
|
465
|
+
method="POST",
|
|
466
|
+
json_data={
|
|
467
|
+
"query": query,
|
|
468
|
+
"top_k": top_k,
|
|
469
|
+
"min_similarity": min_similarity,
|
|
470
|
+
"server": server,
|
|
471
|
+
"cwd": os.getcwd(),
|
|
472
|
+
},
|
|
473
|
+
timeout=120.0, # Embedding generation can be slow
|
|
474
|
+
)
|
|
475
|
+
if result is None:
|
|
476
|
+
sys.exit(1)
|
|
477
|
+
|
|
478
|
+
if json_format:
|
|
479
|
+
click.echo(json.dumps(result, indent=2))
|
|
480
|
+
return
|
|
481
|
+
|
|
482
|
+
results = result.get("results", [])
|
|
483
|
+
if not results:
|
|
484
|
+
click.echo("No matching tools found.")
|
|
485
|
+
return
|
|
486
|
+
|
|
487
|
+
click.echo(f"Found {len(results)} tools matching '{query}':")
|
|
488
|
+
for r in results:
|
|
489
|
+
server_name = r.get("server_name", "unknown")
|
|
490
|
+
tool_name = r.get("tool_name", "unknown")
|
|
491
|
+
similarity = r.get("similarity", 0)
|
|
492
|
+
desc = r.get("description", "")
|
|
493
|
+
click.echo(f" • {server_name}/{tool_name} (similarity: {similarity:.2%})")
|
|
494
|
+
if desc:
|
|
495
|
+
# Truncate long descriptions
|
|
496
|
+
if len(desc) > 80:
|
|
497
|
+
desc = desc[:77] + "..."
|
|
498
|
+
click.echo(f" {desc}")
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
@mcp_proxy.command("import-server")
|
|
502
|
+
@click.option("--from-project", "-p", help="Import from another Gobby project")
|
|
503
|
+
@click.option("--github", "-g", "github_url", help="Import from GitHub repository URL")
|
|
504
|
+
@click.option("--query", "-q", help="Search for MCP server by name/description")
|
|
505
|
+
@click.option("--server", "-s", "servers", multiple=True, help="Specific servers to import")
|
|
506
|
+
@click.option("--json", "json_format", is_flag=True, help="Output as JSON")
|
|
507
|
+
@click.pass_context
|
|
508
|
+
def import_server(
|
|
509
|
+
ctx: click.Context,
|
|
510
|
+
from_project: str | None,
|
|
511
|
+
github_url: str | None,
|
|
512
|
+
query: str | None,
|
|
513
|
+
servers: tuple[str, ...],
|
|
514
|
+
json_format: bool,
|
|
515
|
+
) -> None:
|
|
516
|
+
"""Import MCP server(s) from various sources.
|
|
517
|
+
|
|
518
|
+
Examples:
|
|
519
|
+
gobby mcp-proxy import-server --from-project my-other-project
|
|
520
|
+
gobby mcp-proxy import-server --from-project prod -s context7 -s exa
|
|
521
|
+
gobby mcp-proxy import-server --github https://github.com/user/mcp-server
|
|
522
|
+
gobby mcp-proxy import-server --query "supabase mcp server"
|
|
523
|
+
"""
|
|
524
|
+
client = get_daemon_client(ctx)
|
|
525
|
+
if not check_daemon_running(client):
|
|
526
|
+
sys.exit(1)
|
|
527
|
+
|
|
528
|
+
# Validate that at least one source is specified
|
|
529
|
+
if not from_project and not github_url and not query:
|
|
530
|
+
click.echo(
|
|
531
|
+
"Error: Specify at least one source: --from-project, --github, or --query",
|
|
532
|
+
err=True,
|
|
533
|
+
)
|
|
534
|
+
sys.exit(1)
|
|
535
|
+
|
|
536
|
+
result = call_mcp_api(
|
|
537
|
+
client,
|
|
538
|
+
"/mcp/servers/import",
|
|
539
|
+
method="POST",
|
|
540
|
+
json_data={
|
|
541
|
+
"from_project": from_project,
|
|
542
|
+
"github_url": github_url,
|
|
543
|
+
"query": query,
|
|
544
|
+
"servers": list(servers) if servers else None,
|
|
545
|
+
},
|
|
546
|
+
)
|
|
547
|
+
if result is None:
|
|
548
|
+
sys.exit(1)
|
|
549
|
+
|
|
550
|
+
if json_format:
|
|
551
|
+
click.echo(json.dumps(result, indent=2))
|
|
552
|
+
return
|
|
553
|
+
|
|
554
|
+
# Handle different result statuses
|
|
555
|
+
if result.get("status") == "needs_configuration":
|
|
556
|
+
click.echo("Server configuration extracted but needs secrets:")
|
|
557
|
+
config = result.get("config", {})
|
|
558
|
+
click.echo(f" Name: {config.get('name')}")
|
|
559
|
+
click.echo(f" Transport: {config.get('transport')}")
|
|
560
|
+
missing = result.get("missing", [])
|
|
561
|
+
if missing:
|
|
562
|
+
click.echo(f" Missing secrets: {', '.join(missing)}")
|
|
563
|
+
if result.get("instructions"):
|
|
564
|
+
click.echo(f"\nInstructions:\n{result['instructions']}")
|
|
565
|
+
click.echo("\nUse 'gobby mcp-proxy add-server' to add with required values.")
|
|
566
|
+
return
|
|
567
|
+
|
|
568
|
+
if result.get("success"):
|
|
569
|
+
imported = result.get("imported", [])
|
|
570
|
+
if imported:
|
|
571
|
+
click.echo(f"Imported {len(imported)} server(s):")
|
|
572
|
+
for name in imported:
|
|
573
|
+
click.echo(f" + {name}")
|
|
574
|
+
|
|
575
|
+
skipped = result.get("skipped", [])
|
|
576
|
+
if skipped:
|
|
577
|
+
click.echo(f"Skipped {len(skipped)} existing server(s):")
|
|
578
|
+
for name in skipped:
|
|
579
|
+
click.echo(f" - {name}")
|
|
580
|
+
|
|
581
|
+
failed = result.get("failed", [])
|
|
582
|
+
if failed:
|
|
583
|
+
click.echo(f"Failed to import {len(failed)} server(s):")
|
|
584
|
+
for item in failed:
|
|
585
|
+
click.echo(f" x {item.get('name')}: {item.get('error')}")
|
|
586
|
+
else:
|
|
587
|
+
click.echo(f"Error: {result.get('error', 'Import failed')}", err=True)
|
|
588
|
+
if result.get("available_projects"):
|
|
589
|
+
click.echo(f"Available projects: {', '.join(result['available_projects'])}")
|
|
590
|
+
sys.exit(1)
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
@mcp_proxy.command("refresh")
|
|
594
|
+
@click.option("--force", "-f", is_flag=True, help="Force full refresh, ignore cached hashes")
|
|
595
|
+
@click.option("--server", "-s", help="Only refresh a specific server")
|
|
596
|
+
@click.option("--json", "json_format", is_flag=True, help="Output as JSON")
|
|
597
|
+
@click.pass_context
|
|
598
|
+
def refresh_tools(ctx: click.Context, force: bool, server: str | None, json_format: bool) -> None:
|
|
599
|
+
"""Refresh MCP tools - detect schema changes and re-index.
|
|
600
|
+
|
|
601
|
+
Scans all connected MCP servers for tool schema changes and regenerates
|
|
602
|
+
embeddings for new or modified tools. Unchanged tools are skipped.
|
|
603
|
+
|
|
604
|
+
Examples:
|
|
605
|
+
gobby mcp-proxy refresh
|
|
606
|
+
gobby mcp-proxy refresh --force
|
|
607
|
+
gobby mcp-proxy refresh --server context7
|
|
608
|
+
"""
|
|
609
|
+
import os
|
|
610
|
+
|
|
611
|
+
client = get_daemon_client(ctx)
|
|
612
|
+
if not check_daemon_running(client):
|
|
613
|
+
sys.exit(1)
|
|
614
|
+
|
|
615
|
+
result = call_mcp_api(
|
|
616
|
+
client,
|
|
617
|
+
"/mcp/refresh",
|
|
618
|
+
method="POST",
|
|
619
|
+
json_data={
|
|
620
|
+
"cwd": os.getcwd(),
|
|
621
|
+
"force": force,
|
|
622
|
+
"server": server,
|
|
623
|
+
},
|
|
624
|
+
timeout=300.0, # Embedding generation can be slow
|
|
625
|
+
)
|
|
626
|
+
if result is None:
|
|
627
|
+
sys.exit(1)
|
|
628
|
+
|
|
629
|
+
if json_format:
|
|
630
|
+
click.echo(json.dumps(result, indent=2))
|
|
631
|
+
return
|
|
632
|
+
|
|
633
|
+
if not result.get("success"):
|
|
634
|
+
click.echo(f"Error: {result.get('error', 'Refresh failed')}", err=True)
|
|
635
|
+
sys.exit(1)
|
|
636
|
+
|
|
637
|
+
stats = result.get("stats", {})
|
|
638
|
+
|
|
639
|
+
# Summary
|
|
640
|
+
click.echo("MCP Tools Refresh Complete")
|
|
641
|
+
click.echo(f" Servers processed: {stats.get('servers_processed', 0)}")
|
|
642
|
+
click.echo(f" New tools: {stats.get('tools_new', 0)}")
|
|
643
|
+
click.echo(f" Changed tools: {stats.get('tools_changed', 0)}")
|
|
644
|
+
click.echo(f" Unchanged tools: {stats.get('tools_unchanged', 0)}")
|
|
645
|
+
click.echo(f" Removed tools: {stats.get('tools_removed', 0)}")
|
|
646
|
+
click.echo(f" Embeddings generated: {stats.get('embeddings_generated', 0)}")
|
|
647
|
+
|
|
648
|
+
if force:
|
|
649
|
+
click.echo("\n(--force: all tools treated as new)")
|
|
650
|
+
|
|
651
|
+
# Per-server breakdown
|
|
652
|
+
by_server = stats.get("by_server", {})
|
|
653
|
+
if by_server:
|
|
654
|
+
click.echo("\nBy Server:")
|
|
655
|
+
for srv_name, srv_stats in by_server.items():
|
|
656
|
+
if "error" in srv_stats:
|
|
657
|
+
click.echo(f" ✗ {srv_name}: {srv_stats['error']}")
|
|
658
|
+
else:
|
|
659
|
+
new = srv_stats.get("new", 0)
|
|
660
|
+
changed = srv_stats.get("changed", 0)
|
|
661
|
+
unchanged = srv_stats.get("unchanged", 0)
|
|
662
|
+
click.echo(f" ● {srv_name}: {new} new, {changed} changed, {unchanged} unchanged")
|
|
663
|
+
|
|
664
|
+
|
|
665
|
+
@mcp_proxy.command("status")
|
|
666
|
+
@click.option("--json", "json_format", is_flag=True, help="Output as JSON")
|
|
667
|
+
@click.pass_context
|
|
668
|
+
def proxy_status(ctx: click.Context, json_format: bool) -> None:
|
|
669
|
+
"""Show MCP proxy status and health."""
|
|
670
|
+
client = get_daemon_client(ctx)
|
|
671
|
+
if not check_daemon_running(client):
|
|
672
|
+
sys.exit(1)
|
|
673
|
+
|
|
674
|
+
result = call_mcp_api(client, "/mcp/status", method="GET")
|
|
675
|
+
if result is None:
|
|
676
|
+
sys.exit(1)
|
|
677
|
+
|
|
678
|
+
if json_format:
|
|
679
|
+
click.echo(json.dumps(result, indent=2))
|
|
680
|
+
return
|
|
681
|
+
|
|
682
|
+
click.echo("MCP Proxy Status:")
|
|
683
|
+
click.echo(f" Servers: {result.get('total_servers', 0)}")
|
|
684
|
+
click.echo(f" Connected: {result.get('connected_servers', 0)}")
|
|
685
|
+
click.echo(f" Tools cached: {result.get('cached_tools', 0)}")
|
|
686
|
+
|
|
687
|
+
health = result.get("server_health", {})
|
|
688
|
+
if health:
|
|
689
|
+
click.echo("\nServer Health:")
|
|
690
|
+
for name, info in health.items():
|
|
691
|
+
state = info.get("state", "unknown")
|
|
692
|
+
health_status = info.get("health", "unknown")
|
|
693
|
+
failures = info.get("failures", 0)
|
|
694
|
+
icon = "●" if state == "connected" else "○"
|
|
695
|
+
click.echo(f" {icon} {name}: {state} ({health_status})", nl=False)
|
|
696
|
+
if failures > 0:
|
|
697
|
+
click.echo(f" - {failures} failures", nl=False)
|
|
698
|
+
click.echo()
|