gobby 0.2.5__py3-none-any.whl → 0.2.7__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- gobby/__init__.py +1 -1
- gobby/adapters/__init__.py +2 -1
- gobby/adapters/claude_code.py +13 -4
- gobby/adapters/codex_impl/__init__.py +28 -0
- gobby/adapters/codex_impl/adapter.py +722 -0
- gobby/adapters/codex_impl/client.py +679 -0
- gobby/adapters/codex_impl/protocol.py +20 -0
- gobby/adapters/codex_impl/types.py +68 -0
- gobby/agents/definitions.py +11 -1
- gobby/agents/isolation.py +395 -0
- gobby/agents/runner.py +8 -0
- gobby/agents/sandbox.py +261 -0
- gobby/agents/spawn.py +42 -287
- gobby/agents/spawn_executor.py +385 -0
- gobby/agents/spawners/__init__.py +24 -0
- gobby/agents/spawners/command_builder.py +189 -0
- gobby/agents/spawners/embedded.py +21 -2
- gobby/agents/spawners/headless.py +21 -2
- gobby/agents/spawners/prompt_manager.py +125 -0
- gobby/cli/__init__.py +6 -0
- gobby/cli/clones.py +419 -0
- gobby/cli/conductor.py +266 -0
- gobby/cli/install.py +4 -4
- gobby/cli/installers/antigravity.py +3 -9
- gobby/cli/installers/claude.py +15 -9
- gobby/cli/installers/codex.py +2 -8
- gobby/cli/installers/gemini.py +8 -8
- gobby/cli/installers/shared.py +175 -13
- gobby/cli/sessions.py +1 -1
- gobby/cli/skills.py +858 -0
- gobby/cli/tasks/ai.py +0 -440
- gobby/cli/tasks/crud.py +44 -6
- gobby/cli/tasks/main.py +0 -4
- gobby/cli/tui.py +2 -2
- gobby/cli/utils.py +12 -5
- gobby/clones/__init__.py +13 -0
- gobby/clones/git.py +547 -0
- gobby/conductor/__init__.py +16 -0
- gobby/conductor/alerts.py +135 -0
- gobby/conductor/loop.py +164 -0
- gobby/conductor/monitors/__init__.py +11 -0
- gobby/conductor/monitors/agents.py +116 -0
- gobby/conductor/monitors/tasks.py +155 -0
- gobby/conductor/pricing.py +234 -0
- gobby/conductor/token_tracker.py +160 -0
- gobby/config/__init__.py +12 -97
- gobby/config/app.py +69 -91
- gobby/config/extensions.py +2 -2
- gobby/config/features.py +7 -130
- gobby/config/search.py +110 -0
- gobby/config/servers.py +1 -1
- gobby/config/skills.py +43 -0
- gobby/config/tasks.py +9 -41
- gobby/hooks/__init__.py +0 -13
- gobby/hooks/event_handlers.py +188 -2
- gobby/hooks/hook_manager.py +50 -4
- gobby/hooks/plugins.py +1 -1
- gobby/hooks/skill_manager.py +130 -0
- gobby/hooks/webhooks.py +1 -1
- gobby/install/claude/hooks/hook_dispatcher.py +4 -4
- gobby/install/codex/hooks/hook_dispatcher.py +1 -1
- gobby/install/gemini/hooks/hook_dispatcher.py +87 -12
- gobby/llm/claude.py +22 -34
- gobby/llm/claude_executor.py +46 -256
- gobby/llm/codex_executor.py +59 -291
- gobby/llm/executor.py +21 -0
- gobby/llm/gemini.py +134 -110
- gobby/llm/litellm_executor.py +143 -6
- gobby/llm/resolver.py +98 -35
- gobby/mcp_proxy/importer.py +62 -4
- gobby/mcp_proxy/instructions.py +56 -0
- gobby/mcp_proxy/models.py +15 -0
- gobby/mcp_proxy/registries.py +68 -8
- gobby/mcp_proxy/server.py +33 -3
- gobby/mcp_proxy/services/recommendation.py +43 -11
- gobby/mcp_proxy/services/tool_proxy.py +81 -1
- gobby/mcp_proxy/stdio.py +2 -1
- gobby/mcp_proxy/tools/__init__.py +0 -2
- gobby/mcp_proxy/tools/agent_messaging.py +317 -0
- gobby/mcp_proxy/tools/agents.py +31 -731
- gobby/mcp_proxy/tools/clones.py +518 -0
- gobby/mcp_proxy/tools/memory.py +3 -26
- gobby/mcp_proxy/tools/metrics.py +65 -1
- gobby/mcp_proxy/tools/orchestration/__init__.py +3 -0
- gobby/mcp_proxy/tools/orchestration/cleanup.py +151 -0
- gobby/mcp_proxy/tools/orchestration/wait.py +467 -0
- gobby/mcp_proxy/tools/sessions/__init__.py +14 -0
- gobby/mcp_proxy/tools/sessions/_commits.py +232 -0
- gobby/mcp_proxy/tools/sessions/_crud.py +253 -0
- gobby/mcp_proxy/tools/sessions/_factory.py +63 -0
- gobby/mcp_proxy/tools/sessions/_handoff.py +499 -0
- gobby/mcp_proxy/tools/sessions/_messages.py +138 -0
- gobby/mcp_proxy/tools/skills/__init__.py +616 -0
- gobby/mcp_proxy/tools/spawn_agent.py +417 -0
- gobby/mcp_proxy/tools/task_orchestration.py +7 -0
- gobby/mcp_proxy/tools/task_readiness.py +14 -0
- gobby/mcp_proxy/tools/task_sync.py +1 -1
- gobby/mcp_proxy/tools/tasks/_context.py +0 -20
- gobby/mcp_proxy/tools/tasks/_crud.py +91 -4
- gobby/mcp_proxy/tools/tasks/_expansion.py +348 -0
- gobby/mcp_proxy/tools/tasks/_factory.py +6 -16
- gobby/mcp_proxy/tools/tasks/_lifecycle.py +110 -45
- gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +18 -29
- gobby/mcp_proxy/tools/workflows.py +1 -1
- gobby/mcp_proxy/tools/worktrees.py +0 -338
- gobby/memory/backends/__init__.py +6 -1
- gobby/memory/backends/mem0.py +6 -1
- gobby/memory/extractor.py +477 -0
- gobby/memory/ingestion/__init__.py +5 -0
- gobby/memory/ingestion/multimodal.py +221 -0
- gobby/memory/manager.py +73 -285
- gobby/memory/search/__init__.py +10 -0
- gobby/memory/search/coordinator.py +248 -0
- gobby/memory/services/__init__.py +5 -0
- gobby/memory/services/crossref.py +142 -0
- gobby/prompts/loader.py +5 -2
- gobby/runner.py +37 -16
- gobby/search/__init__.py +48 -6
- gobby/search/backends/__init__.py +159 -0
- gobby/search/backends/embedding.py +225 -0
- gobby/search/embeddings.py +238 -0
- gobby/search/models.py +148 -0
- gobby/search/unified.py +496 -0
- gobby/servers/http.py +24 -12
- gobby/servers/routes/admin.py +294 -0
- gobby/servers/routes/mcp/endpoints/__init__.py +61 -0
- gobby/servers/routes/mcp/endpoints/discovery.py +405 -0
- gobby/servers/routes/mcp/endpoints/execution.py +568 -0
- gobby/servers/routes/mcp/endpoints/registry.py +378 -0
- gobby/servers/routes/mcp/endpoints/server.py +304 -0
- gobby/servers/routes/mcp/hooks.py +1 -1
- gobby/servers/routes/mcp/tools.py +48 -1317
- gobby/servers/websocket.py +2 -2
- gobby/sessions/analyzer.py +2 -0
- gobby/sessions/lifecycle.py +1 -1
- gobby/sessions/processor.py +10 -0
- gobby/sessions/transcripts/base.py +2 -0
- gobby/sessions/transcripts/claude.py +79 -10
- gobby/skills/__init__.py +91 -0
- gobby/skills/loader.py +685 -0
- gobby/skills/manager.py +384 -0
- gobby/skills/parser.py +286 -0
- gobby/skills/search.py +463 -0
- gobby/skills/sync.py +119 -0
- gobby/skills/updater.py +385 -0
- gobby/skills/validator.py +368 -0
- gobby/storage/clones.py +378 -0
- gobby/storage/database.py +1 -1
- gobby/storage/memories.py +43 -13
- gobby/storage/migrations.py +162 -201
- gobby/storage/sessions.py +116 -7
- gobby/storage/skills.py +782 -0
- gobby/storage/tasks/_crud.py +4 -4
- gobby/storage/tasks/_lifecycle.py +57 -7
- gobby/storage/tasks/_manager.py +14 -5
- gobby/storage/tasks/_models.py +8 -3
- gobby/sync/memories.py +40 -5
- gobby/sync/tasks.py +83 -6
- gobby/tasks/__init__.py +1 -2
- gobby/tasks/external_validator.py +1 -1
- gobby/tasks/validation.py +46 -35
- gobby/tools/summarizer.py +91 -10
- gobby/tui/api_client.py +4 -7
- gobby/tui/app.py +5 -3
- gobby/tui/screens/orchestrator.py +1 -2
- gobby/tui/screens/tasks.py +2 -4
- gobby/tui/ws_client.py +1 -1
- gobby/utils/daemon_client.py +2 -2
- gobby/utils/project_context.py +2 -3
- gobby/utils/status.py +13 -0
- gobby/workflows/actions.py +221 -1135
- gobby/workflows/artifact_actions.py +31 -0
- gobby/workflows/autonomous_actions.py +11 -0
- gobby/workflows/context_actions.py +93 -1
- gobby/workflows/detection_helpers.py +115 -31
- gobby/workflows/enforcement/__init__.py +47 -0
- gobby/workflows/enforcement/blocking.py +269 -0
- gobby/workflows/enforcement/commit_policy.py +283 -0
- gobby/workflows/enforcement/handlers.py +269 -0
- gobby/workflows/{task_enforcement_actions.py → enforcement/task_policy.py} +29 -388
- gobby/workflows/engine.py +13 -2
- gobby/workflows/git_utils.py +106 -0
- gobby/workflows/lifecycle_evaluator.py +29 -1
- gobby/workflows/llm_actions.py +30 -0
- gobby/workflows/loader.py +19 -6
- gobby/workflows/mcp_actions.py +20 -1
- gobby/workflows/memory_actions.py +154 -0
- gobby/workflows/safe_evaluator.py +183 -0
- gobby/workflows/session_actions.py +44 -0
- gobby/workflows/state_actions.py +60 -1
- gobby/workflows/stop_signal_actions.py +55 -0
- gobby/workflows/summary_actions.py +111 -1
- gobby/workflows/task_sync_actions.py +347 -0
- gobby/workflows/todo_actions.py +34 -1
- gobby/workflows/webhook_actions.py +185 -0
- {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/METADATA +87 -21
- {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/RECORD +201 -172
- {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/WHEEL +1 -1
- gobby/adapters/codex.py +0 -1292
- gobby/install/claude/commands/gobby/bug.md +0 -51
- gobby/install/claude/commands/gobby/chore.md +0 -51
- gobby/install/claude/commands/gobby/epic.md +0 -52
- gobby/install/claude/commands/gobby/eval.md +0 -235
- gobby/install/claude/commands/gobby/feat.md +0 -49
- gobby/install/claude/commands/gobby/nit.md +0 -52
- gobby/install/claude/commands/gobby/ref.md +0 -52
- gobby/install/codex/prompts/forget.md +0 -7
- gobby/install/codex/prompts/memories.md +0 -7
- gobby/install/codex/prompts/recall.md +0 -7
- gobby/install/codex/prompts/remember.md +0 -13
- gobby/llm/gemini_executor.py +0 -339
- gobby/mcp_proxy/tools/session_messages.py +0 -1056
- gobby/mcp_proxy/tools/task_expansion.py +0 -591
- gobby/prompts/defaults/expansion/system.md +0 -119
- gobby/prompts/defaults/expansion/user.md +0 -48
- gobby/prompts/defaults/external_validation/agent.md +0 -72
- gobby/prompts/defaults/external_validation/external.md +0 -63
- gobby/prompts/defaults/external_validation/spawn.md +0 -83
- gobby/prompts/defaults/external_validation/system.md +0 -6
- gobby/prompts/defaults/features/import_mcp.md +0 -22
- gobby/prompts/defaults/features/import_mcp_github.md +0 -17
- gobby/prompts/defaults/features/import_mcp_search.md +0 -16
- gobby/prompts/defaults/features/recommend_tools.md +0 -32
- gobby/prompts/defaults/features/recommend_tools_hybrid.md +0 -35
- gobby/prompts/defaults/features/recommend_tools_llm.md +0 -30
- gobby/prompts/defaults/features/server_description.md +0 -20
- gobby/prompts/defaults/features/server_description_system.md +0 -6
- gobby/prompts/defaults/features/task_description.md +0 -31
- gobby/prompts/defaults/features/task_description_system.md +0 -6
- gobby/prompts/defaults/features/tool_summary.md +0 -17
- gobby/prompts/defaults/features/tool_summary_system.md +0 -6
- gobby/prompts/defaults/research/step.md +0 -58
- gobby/prompts/defaults/validation/criteria.md +0 -47
- gobby/prompts/defaults/validation/validate.md +0 -38
- gobby/storage/migrations_legacy.py +0 -1359
- gobby/tasks/context.py +0 -747
- gobby/tasks/criteria.py +0 -342
- gobby/tasks/expansion.py +0 -626
- gobby/tasks/prompts/expand.py +0 -327
- gobby/tasks/research.py +0 -421
- gobby/tasks/tdd.py +0 -352
- {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/entry_points.txt +0 -0
- {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/licenses/LICENSE.md +0 -0
- {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/top_level.txt +0 -0
gobby/llm/resolver.py
CHANGED
|
@@ -18,7 +18,8 @@ from typing import TYPE_CHECKING, Literal
|
|
|
18
18
|
from gobby.llm.executor import AgentExecutor
|
|
19
19
|
|
|
20
20
|
if TYPE_CHECKING:
|
|
21
|
-
from gobby.config.app import DaemonConfig
|
|
21
|
+
from gobby.config.app import DaemonConfig
|
|
22
|
+
from gobby.config.llm_providers import LLMProvidersConfig
|
|
22
23
|
from gobby.workflows.definitions import WorkflowDefinition
|
|
23
24
|
|
|
24
25
|
logger = logging.getLogger(__name__)
|
|
@@ -259,8 +260,13 @@ def create_executor(
|
|
|
259
260
|
"""
|
|
260
261
|
Create an AgentExecutor for the given provider.
|
|
261
262
|
|
|
263
|
+
Routing strategy:
|
|
264
|
+
- api_key and adc auth modes: Route to LiteLLMExecutor for unified cost tracking
|
|
265
|
+
- subscription mode (Claude): Route to ClaudeExecutor (Claude Agent SDK)
|
|
266
|
+
- cli mode (Codex): Route to CodexExecutor (Codex CLI subprocess)
|
|
267
|
+
|
|
262
268
|
Args:
|
|
263
|
-
provider: Provider name (claude, gemini, litellm).
|
|
269
|
+
provider: Provider name (claude, gemini, litellm, codex).
|
|
264
270
|
config: Optional daemon config for provider settings.
|
|
265
271
|
model: Optional model override.
|
|
266
272
|
|
|
@@ -279,19 +285,40 @@ def create_executor(
|
|
|
279
285
|
if config and config.llm_providers:
|
|
280
286
|
provider_config = getattr(config.llm_providers, provider, None)
|
|
281
287
|
|
|
288
|
+
# Determine auth_mode from config
|
|
289
|
+
auth_mode = "api_key" # Default
|
|
290
|
+
if provider_config:
|
|
291
|
+
auth_mode = getattr(provider_config, "auth_mode", "api_key") or "api_key"
|
|
292
|
+
|
|
282
293
|
try:
|
|
283
|
-
|
|
294
|
+
# Route based on auth_mode:
|
|
295
|
+
# - subscription (Claude) -> ClaudeExecutor
|
|
296
|
+
# - cli (Codex) -> CodexExecutor
|
|
297
|
+
# - api_key/adc (all providers) -> LiteLLMExecutor
|
|
298
|
+
|
|
299
|
+
if provider == "claude" and auth_mode == "subscription":
|
|
300
|
+
# Subscription mode requires Claude Agent SDK
|
|
284
301
|
return _create_claude_executor(provider_config, model)
|
|
285
|
-
|
|
286
|
-
|
|
302
|
+
|
|
303
|
+
elif provider == "codex" and auth_mode in ("subscription", "cli"):
|
|
304
|
+
# CLI mode requires Codex CLI subprocess
|
|
305
|
+
return _create_codex_executor(provider_config, model, auth_mode)
|
|
306
|
+
|
|
287
307
|
elif provider == "litellm":
|
|
308
|
+
# Direct LiteLLM usage
|
|
288
309
|
return _create_litellm_executor(provider_config, config, model)
|
|
289
|
-
|
|
290
|
-
|
|
310
|
+
|
|
311
|
+
elif auth_mode in ("api_key", "adc"):
|
|
312
|
+
# Route all api_key and adc modes through LiteLLM for unified cost tracking
|
|
313
|
+
return _create_litellm_executor_for_provider(
|
|
314
|
+
provider, auth_mode, provider_config, config, model
|
|
315
|
+
)
|
|
316
|
+
|
|
291
317
|
else:
|
|
292
318
|
raise ExecutorCreationError(
|
|
293
319
|
provider,
|
|
294
|
-
f"Unknown provider
|
|
320
|
+
f"Unknown provider/auth_mode combination: {provider}/{auth_mode}. "
|
|
321
|
+
f"Supported: {list(SUPPORTED_PROVIDERS)}",
|
|
295
322
|
)
|
|
296
323
|
except ProviderError:
|
|
297
324
|
raise
|
|
@@ -303,15 +330,18 @@ def _create_claude_executor(
|
|
|
303
330
|
provider_config: "LLMProviderConfig | None",
|
|
304
331
|
model: str | None,
|
|
305
332
|
) -> AgentExecutor:
|
|
306
|
-
"""
|
|
333
|
+
"""
|
|
334
|
+
Create ClaudeExecutor for subscription mode only.
|
|
335
|
+
|
|
336
|
+
Note: api_key mode is now routed through LiteLLMExecutor for unified cost tracking.
|
|
337
|
+
This function should only be called when auth_mode is "subscription".
|
|
338
|
+
"""
|
|
307
339
|
from gobby.llm.claude_executor import ClaudeExecutor
|
|
308
340
|
|
|
309
|
-
#
|
|
310
|
-
auth_mode = "api_key"
|
|
341
|
+
# Subscription mode only - api_key mode routes through LiteLLM
|
|
311
342
|
default_model = "claude-sonnet-4-20250514"
|
|
312
343
|
|
|
313
344
|
if provider_config:
|
|
314
|
-
auth_mode = getattr(provider_config, "auth_mode", "api_key") or "api_key"
|
|
315
345
|
# Get first model from comma-separated list if set
|
|
316
346
|
models_str = getattr(provider_config, "models", None)
|
|
317
347
|
if models_str:
|
|
@@ -320,46 +350,71 @@ def _create_claude_executor(
|
|
|
320
350
|
default_model = models[0]
|
|
321
351
|
|
|
322
352
|
return ClaudeExecutor(
|
|
323
|
-
auth_mode=
|
|
353
|
+
auth_mode="subscription",
|
|
324
354
|
default_model=model or default_model,
|
|
325
355
|
)
|
|
326
356
|
|
|
327
357
|
|
|
328
|
-
def
|
|
358
|
+
def _create_litellm_executor(
|
|
329
359
|
provider_config: "LLMProviderConfig | None",
|
|
360
|
+
config: "DaemonConfig | None",
|
|
330
361
|
model: str | None,
|
|
331
362
|
) -> AgentExecutor:
|
|
332
|
-
"""Create
|
|
333
|
-
from gobby.llm.
|
|
363
|
+
"""Create LiteLLMExecutor with API keys from config (direct litellm usage)."""
|
|
364
|
+
from gobby.llm.litellm_executor import LiteLLMExecutor
|
|
334
365
|
|
|
335
|
-
# Determine
|
|
336
|
-
|
|
337
|
-
|
|
366
|
+
# Determine model and API base from config
|
|
367
|
+
default_model = "gpt-4o-mini"
|
|
368
|
+
api_base = None
|
|
369
|
+
api_keys: dict[str, str] | None = None
|
|
338
370
|
|
|
339
371
|
if provider_config:
|
|
340
|
-
auth_mode = getattr(provider_config, "auth_mode", "api_key") or "api_key"
|
|
341
372
|
models_str = getattr(provider_config, "models", None)
|
|
342
373
|
if models_str:
|
|
343
374
|
models = [m.strip() for m in models_str.split(",") if m.strip()]
|
|
344
375
|
if models:
|
|
345
376
|
default_model = models[0]
|
|
377
|
+
api_base = getattr(provider_config, "api_base", None)
|
|
346
378
|
|
|
347
|
-
|
|
348
|
-
|
|
379
|
+
# Get API keys from llm_providers.api_keys
|
|
380
|
+
if config and config.llm_providers:
|
|
381
|
+
api_keys = config.llm_providers.api_keys or None
|
|
382
|
+
|
|
383
|
+
return LiteLLMExecutor(
|
|
349
384
|
default_model=model or default_model,
|
|
385
|
+
api_base=api_base,
|
|
386
|
+
api_keys=api_keys,
|
|
350
387
|
)
|
|
351
388
|
|
|
352
389
|
|
|
353
|
-
def
|
|
390
|
+
def _create_litellm_executor_for_provider(
|
|
391
|
+
provider: str,
|
|
392
|
+
auth_mode: str,
|
|
354
393
|
provider_config: "LLMProviderConfig | None",
|
|
355
394
|
config: "DaemonConfig | None",
|
|
356
395
|
model: str | None,
|
|
357
396
|
) -> AgentExecutor:
|
|
358
|
-
"""
|
|
397
|
+
"""
|
|
398
|
+
Create LiteLLMExecutor configured for a specific provider's api_key/adc mode.
|
|
399
|
+
|
|
400
|
+
This routes provider-specific calls through LiteLLM for unified cost tracking:
|
|
401
|
+
- Claude (api_key) -> anthropic/model
|
|
402
|
+
- Gemini (api_key) -> gemini/model
|
|
403
|
+
- Gemini (adc) -> vertex_ai/model
|
|
404
|
+
- Codex/OpenAI (api_key) -> model (no prefix)
|
|
405
|
+
"""
|
|
359
406
|
from gobby.llm.litellm_executor import LiteLLMExecutor
|
|
360
407
|
|
|
361
|
-
#
|
|
362
|
-
|
|
408
|
+
# Default models per provider
|
|
409
|
+
default_models = {
|
|
410
|
+
"claude": "claude-sonnet-4-20250514",
|
|
411
|
+
"gemini": "gemini-2.0-flash",
|
|
412
|
+
"codex": "gpt-4o",
|
|
413
|
+
"openai": "gpt-4o",
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
# Determine model from config
|
|
417
|
+
default_model = default_models.get(provider, "gpt-4o-mini")
|
|
363
418
|
api_base = None
|
|
364
419
|
api_keys: dict[str, str] | None = None
|
|
365
420
|
|
|
@@ -375,34 +430,42 @@ def _create_litellm_executor(
|
|
|
375
430
|
if config and config.llm_providers:
|
|
376
431
|
api_keys = config.llm_providers.api_keys or None
|
|
377
432
|
|
|
433
|
+
# Cast auth_mode to the expected literal type
|
|
434
|
+
litellm_auth_mode = auth_mode if auth_mode in ("api_key", "adc") else "api_key"
|
|
435
|
+
|
|
378
436
|
return LiteLLMExecutor(
|
|
379
437
|
default_model=model or default_model,
|
|
380
438
|
api_base=api_base,
|
|
381
439
|
api_keys=api_keys,
|
|
440
|
+
provider=provider, # type: ignore[arg-type]
|
|
441
|
+
auth_mode=litellm_auth_mode, # type: ignore[arg-type]
|
|
382
442
|
)
|
|
383
443
|
|
|
384
444
|
|
|
385
445
|
def _create_codex_executor(
|
|
386
446
|
provider_config: "LLMProviderConfig | None",
|
|
387
447
|
model: str | None,
|
|
448
|
+
auth_mode: str = "subscription",
|
|
388
449
|
) -> AgentExecutor:
|
|
389
450
|
"""
|
|
390
|
-
Create CodexExecutor
|
|
451
|
+
Create CodexExecutor for subscription/CLI mode only.
|
|
452
|
+
|
|
453
|
+
Note: api_key mode is now routed through LiteLLMExecutor for unified cost tracking.
|
|
454
|
+
This function should only be called when auth_mode is "subscription" or "cli".
|
|
391
455
|
|
|
392
|
-
Codex
|
|
393
|
-
- api_key: OpenAI API with function calling (full tool injection)
|
|
394
|
-
- subscription: Codex CLI with ChatGPT subscription (no custom tools)
|
|
456
|
+
CLI mode uses Codex CLI subprocess - no custom tool injection supported.
|
|
395
457
|
|
|
396
|
-
|
|
458
|
+
Args:
|
|
459
|
+
provider_config: Provider configuration.
|
|
460
|
+
model: Optional model override.
|
|
461
|
+
auth_mode: Authentication mode - "subscription" or "cli".
|
|
397
462
|
"""
|
|
398
463
|
from gobby.llm.codex_executor import CodexExecutor
|
|
399
464
|
|
|
400
|
-
#
|
|
401
|
-
auth_mode = "api_key"
|
|
465
|
+
# CLI/subscription mode only - api_key mode routes through LiteLLM
|
|
402
466
|
default_model = "gpt-4o"
|
|
403
467
|
|
|
404
468
|
if provider_config:
|
|
405
|
-
auth_mode = getattr(provider_config, "auth_mode", "api_key") or "api_key"
|
|
406
469
|
models_str = getattr(provider_config, "models", None)
|
|
407
470
|
if models_str:
|
|
408
471
|
models = [m.strip() for m in models_str.split(",") if m.strip()]
|
|
@@ -417,7 +480,7 @@ def _create_codex_executor(
|
|
|
417
480
|
|
|
418
481
|
# Re-export for TYPE_CHECKING
|
|
419
482
|
if TYPE_CHECKING:
|
|
420
|
-
from gobby.config.
|
|
483
|
+
from gobby.config.llm_providers import LLMProviderConfig
|
|
421
484
|
|
|
422
485
|
|
|
423
486
|
class ExecutorRegistry:
|
gobby/mcp_proxy/importer.py
CHANGED
|
@@ -5,6 +5,8 @@ import re
|
|
|
5
5
|
from typing import TYPE_CHECKING, Any
|
|
6
6
|
|
|
7
7
|
from gobby.config.app import DaemonConfig
|
|
8
|
+
from gobby.config.features import DEFAULT_IMPORT_MCP_SERVER_PROMPT
|
|
9
|
+
from gobby.prompts import PromptLoader
|
|
8
10
|
from gobby.storage.database import DatabaseProtocol
|
|
9
11
|
from gobby.storage.mcp import LocalMCPManager
|
|
10
12
|
from gobby.storage.projects import LocalProjectManager
|
|
@@ -18,6 +20,21 @@ logger = logging.getLogger(__name__)
|
|
|
18
20
|
# Pattern to detect placeholder secrets like <YOUR_API_KEY>
|
|
19
21
|
SECRET_PLACEHOLDER_PATTERN = re.compile(r"<YOUR_[A-Z0-9_]+>")
|
|
20
22
|
|
|
23
|
+
DEFAULT_GITHUB_FETCH_PROMPT = """Fetch the README from this GitHub repository and extract MCP server configuration:
|
|
24
|
+
|
|
25
|
+
{github_url}
|
|
26
|
+
|
|
27
|
+
If the URL doesn't point directly to a README, try to find and fetch the README.md file.
|
|
28
|
+
|
|
29
|
+
After reading the documentation, extract the MCP server configuration as a JSON object."""
|
|
30
|
+
|
|
31
|
+
DEFAULT_SEARCH_FETCH_PROMPT = """Search for MCP server: {search_query}
|
|
32
|
+
|
|
33
|
+
Find the official documentation or GitHub repository for this MCP server.
|
|
34
|
+
Then fetch and read the README or installation docs.
|
|
35
|
+
|
|
36
|
+
After reading the documentation, extract the MCP server configuration as a JSON object."""
|
|
37
|
+
|
|
21
38
|
|
|
22
39
|
class MCPServerImporter:
|
|
23
40
|
"""Handles importing MCP servers from various sources."""
|
|
@@ -46,6 +63,21 @@ class MCPServerImporter:
|
|
|
46
63
|
self.mcp_client_manager = mcp_client_manager
|
|
47
64
|
self.import_config = config.get_import_mcp_server_config()
|
|
48
65
|
|
|
66
|
+
# Initialize prompt loader
|
|
67
|
+
project_path = None
|
|
68
|
+
if current_project_id:
|
|
69
|
+
if project := self.project_manager.get(current_project_id):
|
|
70
|
+
project_path = project.repo_path
|
|
71
|
+
|
|
72
|
+
from pathlib import Path
|
|
73
|
+
|
|
74
|
+
self._loader = PromptLoader(project_dir=Path(project_path) if project_path else None)
|
|
75
|
+
|
|
76
|
+
# Register fallbacks
|
|
77
|
+
self._loader.register_fallback("import/github_fetch", lambda: DEFAULT_GITHUB_FETCH_PROMPT)
|
|
78
|
+
self._loader.register_fallback("import/search_fetch", lambda: DEFAULT_SEARCH_FETCH_PROMPT)
|
|
79
|
+
self._loader.register_fallback("import/system", lambda: DEFAULT_IMPORT_MCP_SERVER_PROMPT)
|
|
80
|
+
|
|
49
81
|
async def import_from_project(
|
|
50
82
|
self,
|
|
51
83
|
source_project: str,
|
|
@@ -171,10 +203,23 @@ class MCPServerImporter:
|
|
|
171
203
|
from claude_agent_sdk import AssistantMessage, ClaudeAgentOptions, TextBlock, query
|
|
172
204
|
|
|
173
205
|
# Build prompt to fetch and extract config
|
|
174
|
-
|
|
206
|
+
prompt_path = self.import_config.github_fetch_prompt_path or "import/github_fetch"
|
|
207
|
+
try:
|
|
208
|
+
prompt = self._loader.render(prompt_path, {"github_url": github_url})
|
|
209
|
+
except Exception as e:
|
|
210
|
+
logger.warning(f"Failed to load Github fetch prompt: {e}")
|
|
211
|
+
prompt = DEFAULT_GITHUB_FETCH_PROMPT.format(github_url=github_url)
|
|
212
|
+
|
|
213
|
+
# Get system prompt
|
|
214
|
+
sys_prompt_path = self.import_config.prompt_path or "import/system"
|
|
215
|
+
try:
|
|
216
|
+
system_prompt = self._loader.render(sys_prompt_path, {})
|
|
217
|
+
except Exception as e:
|
|
218
|
+
logger.warning(f"Failed to load import system prompt: {e}")
|
|
219
|
+
system_prompt = DEFAULT_IMPORT_MCP_SERVER_PROMPT
|
|
175
220
|
|
|
176
221
|
options = ClaudeAgentOptions(
|
|
177
|
-
system_prompt=
|
|
222
|
+
system_prompt=system_prompt,
|
|
178
223
|
max_turns=3,
|
|
179
224
|
model=self.import_config.model,
|
|
180
225
|
allowed_tools=["WebFetch"],
|
|
@@ -222,10 +267,23 @@ class MCPServerImporter:
|
|
|
222
267
|
from claude_agent_sdk import AssistantMessage, ClaudeAgentOptions, TextBlock, query
|
|
223
268
|
|
|
224
269
|
# Build prompt to search and extract config
|
|
225
|
-
|
|
270
|
+
prompt_path = self.import_config.search_fetch_prompt_path or "import/search_fetch"
|
|
271
|
+
try:
|
|
272
|
+
prompt = self._loader.render(prompt_path, {"search_query": search_query})
|
|
273
|
+
except Exception as e:
|
|
274
|
+
logger.warning(f"Failed to load search fetch prompt: {e}")
|
|
275
|
+
prompt = DEFAULT_SEARCH_FETCH_PROMPT.format(search_query=search_query)
|
|
276
|
+
|
|
277
|
+
# Get system prompt
|
|
278
|
+
sys_prompt_path = self.import_config.prompt_path or "import/system"
|
|
279
|
+
try:
|
|
280
|
+
system_prompt = self._loader.render(sys_prompt_path, {})
|
|
281
|
+
except Exception as e:
|
|
282
|
+
logger.warning(f"Failed to load import system prompt: {e}")
|
|
283
|
+
system_prompt = DEFAULT_IMPORT_MCP_SERVER_PROMPT
|
|
226
284
|
|
|
227
285
|
options = ClaudeAgentOptions(
|
|
228
|
-
system_prompt=
|
|
286
|
+
system_prompt=system_prompt,
|
|
229
287
|
max_turns=5, # More turns for search + fetch
|
|
230
288
|
model=self.import_config.model,
|
|
231
289
|
allowed_tools=["WebSearch", "WebFetch"],
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Gobby MCP server instructions.
|
|
2
|
+
|
|
3
|
+
Provides XML-structured instructions that teach agents how to use Gobby correctly.
|
|
4
|
+
These instructions are injected into the MCP server via FastMCP's `instructions` parameter.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def build_gobby_instructions() -> str:
|
|
9
|
+
"""Build XML-structured instructions for Gobby MCP server.
|
|
10
|
+
|
|
11
|
+
These instructions teach agents how to use Gobby correctly.
|
|
12
|
+
Every agent connecting to Gobby receives these automatically.
|
|
13
|
+
|
|
14
|
+
The instructions cover:
|
|
15
|
+
- Session startup sequence
|
|
16
|
+
- Progressive tool disclosure pattern
|
|
17
|
+
- Progressive skill disclosure pattern
|
|
18
|
+
- Critical rules for task management
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
XML-structured instructions string
|
|
22
|
+
"""
|
|
23
|
+
return """<gobby_system>
|
|
24
|
+
|
|
25
|
+
<startup>
|
|
26
|
+
At the start of EVERY session:
|
|
27
|
+
1. `list_mcp_servers()` — Discover available servers
|
|
28
|
+
2. `list_skills()` — Discover available skills
|
|
29
|
+
3. Session ID: Look for `session_id: <uuid>` in your context.
|
|
30
|
+
If missing, call:
|
|
31
|
+
`call_tool("gobby-sessions", "get_current", {"external_id": "<your-session-id>", "source": "claude"})`
|
|
32
|
+
|
|
33
|
+
Session and task references use `#N` format (e.g., `#1`, `#42`) which is project-scoped.
|
|
34
|
+
</startup>
|
|
35
|
+
|
|
36
|
+
<tool_discovery>
|
|
37
|
+
NEVER assume tool schemas. Use progressive disclosure:
|
|
38
|
+
1. `list_tools(server="...")` — Lightweight metadata (~100 tokens/tool)
|
|
39
|
+
2. `get_tool_schema(server, tool)` — Full schema when needed
|
|
40
|
+
3. `call_tool(server, tool, args)` — Execute
|
|
41
|
+
</tool_discovery>
|
|
42
|
+
|
|
43
|
+
<skill_discovery>
|
|
44
|
+
Skills provide detailed guidance. Use progressive disclosure:
|
|
45
|
+
1. `list_skills()` — Already done at startup
|
|
46
|
+
2. `get_skill(name="...")` — Full content when needed
|
|
47
|
+
3. `search_skills(query="...")` — Find by task description
|
|
48
|
+
</skill_discovery>
|
|
49
|
+
|
|
50
|
+
<rules>
|
|
51
|
+
- Create/claim a task before using Edit, Write, or NotebookEdit tools
|
|
52
|
+
- Pass session_id to create_task (required), claim_task (required), and close_task (optional, for tracking)
|
|
53
|
+
- NEVER load all tool schemas upfront — use progressive disclosure
|
|
54
|
+
</rules>
|
|
55
|
+
|
|
56
|
+
</gobby_system>"""
|
gobby/mcp_proxy/models.py
CHANGED
|
@@ -31,6 +31,21 @@ class MCPError(Exception):
|
|
|
31
31
|
self.code = code
|
|
32
32
|
|
|
33
33
|
|
|
34
|
+
class ToolProxyErrorCode(str, Enum):
|
|
35
|
+
"""Structured error codes for ToolProxyService responses.
|
|
36
|
+
|
|
37
|
+
Used by _process_tool_proxy_result to determine HTTP status codes
|
|
38
|
+
without fragile string matching.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
SERVER_NOT_FOUND = "SERVER_NOT_FOUND"
|
|
42
|
+
SERVER_NOT_CONFIGURED = "SERVER_NOT_CONFIGURED"
|
|
43
|
+
TOOL_NOT_FOUND = "TOOL_NOT_FOUND"
|
|
44
|
+
INVALID_ARGUMENTS = "INVALID_ARGUMENTS"
|
|
45
|
+
EXECUTION_ERROR = "EXECUTION_ERROR"
|
|
46
|
+
CONNECTION_ERROR = "CONNECTION_ERROR"
|
|
47
|
+
|
|
48
|
+
|
|
34
49
|
class HealthState(str, Enum):
|
|
35
50
|
"""Connection health state for monitoring."""
|
|
36
51
|
|
gobby/mcp_proxy/registries.py
CHANGED
|
@@ -16,13 +16,14 @@ if TYPE_CHECKING:
|
|
|
16
16
|
from gobby.mcp_proxy.services.tool_proxy import ToolProxyService
|
|
17
17
|
from gobby.memory.manager import MemoryManager
|
|
18
18
|
from gobby.sessions.manager import SessionManager
|
|
19
|
+
from gobby.storage.clones import LocalCloneManager
|
|
20
|
+
from gobby.storage.inter_session_messages import InterSessionMessageManager
|
|
19
21
|
from gobby.storage.merge_resolutions import MergeResolutionManager
|
|
20
22
|
from gobby.storage.session_messages import LocalSessionMessageManager
|
|
21
23
|
from gobby.storage.sessions import LocalSessionManager
|
|
22
24
|
from gobby.storage.tasks import LocalTaskManager
|
|
23
25
|
from gobby.storage.worktrees import LocalWorktreeManager
|
|
24
26
|
from gobby.sync.tasks import TaskSyncManager
|
|
25
|
-
from gobby.tasks.expansion import TaskExpander
|
|
26
27
|
from gobby.tasks.validation import TaskValidator
|
|
27
28
|
from gobby.worktrees.git import WorktreeGitManager
|
|
28
29
|
from gobby.worktrees.merge import MergeResolver
|
|
@@ -36,7 +37,6 @@ def setup_internal_registries(
|
|
|
36
37
|
memory_manager: MemoryManager | None = None,
|
|
37
38
|
task_manager: LocalTaskManager | None = None,
|
|
38
39
|
sync_manager: TaskSyncManager | None = None,
|
|
39
|
-
task_expander: TaskExpander | None = None,
|
|
40
40
|
task_validator: TaskValidator | None = None,
|
|
41
41
|
message_manager: LocalSessionMessageManager | None = None,
|
|
42
42
|
local_session_manager: LocalSessionManager | None = None,
|
|
@@ -44,11 +44,13 @@ def setup_internal_registries(
|
|
|
44
44
|
llm_service: LLMService | None = None,
|
|
45
45
|
agent_runner: AgentRunner | None = None,
|
|
46
46
|
worktree_storage: LocalWorktreeManager | None = None,
|
|
47
|
+
clone_storage: LocalCloneManager | None = None,
|
|
47
48
|
git_manager: WorktreeGitManager | None = None,
|
|
48
49
|
merge_storage: MergeResolutionManager | None = None,
|
|
49
50
|
merge_resolver: MergeResolver | None = None,
|
|
50
51
|
project_id: str | None = None,
|
|
51
52
|
tool_proxy_getter: Callable[[], ToolProxyService | None] | None = None,
|
|
53
|
+
inter_session_message_manager: InterSessionMessageManager | None = None,
|
|
52
54
|
) -> InternalRegistryManager:
|
|
53
55
|
"""
|
|
54
56
|
Setup internal MCP registries (tasks, messages, memory, metrics, agents, worktrees).
|
|
@@ -59,7 +61,6 @@ def setup_internal_registries(
|
|
|
59
61
|
memory_manager: Memory manager for memory operations
|
|
60
62
|
task_manager: Task storage manager
|
|
61
63
|
sync_manager: Task sync manager for git sync
|
|
62
|
-
task_expander: Task expander for AI expansion
|
|
63
64
|
task_validator: Task validator for validation
|
|
64
65
|
message_manager: Message storage manager
|
|
65
66
|
local_session_manager: Local session manager for session CRUD
|
|
@@ -73,6 +74,7 @@ def setup_internal_registries(
|
|
|
73
74
|
project_id: Default project ID for worktree operations
|
|
74
75
|
tool_proxy_getter: Callable that returns ToolProxyService for routing
|
|
75
76
|
tool calls in in-process agents. Called lazily during agent execution.
|
|
77
|
+
inter_session_message_manager: Inter-session message manager for agent messaging
|
|
76
78
|
|
|
77
79
|
Returns:
|
|
78
80
|
InternalRegistryManager containing all registries
|
|
@@ -99,7 +101,6 @@ def setup_internal_registries(
|
|
|
99
101
|
tasks_registry = create_task_registry(
|
|
100
102
|
task_manager=task_manager,
|
|
101
103
|
sync_manager=sync_manager,
|
|
102
|
-
task_expander=task_expander,
|
|
103
104
|
task_validator=task_validator,
|
|
104
105
|
config=_config,
|
|
105
106
|
agent_runner=agent_runner,
|
|
@@ -113,7 +114,7 @@ def setup_internal_registries(
|
|
|
113
114
|
# Initialize sessions registry (messages + session CRUD)
|
|
114
115
|
# Register if either message_manager or local_session_manager is available
|
|
115
116
|
if message_manager is not None or local_session_manager is not None:
|
|
116
|
-
from gobby.mcp_proxy.tools.
|
|
117
|
+
from gobby.mcp_proxy.tools.sessions import create_session_messages_registry
|
|
117
118
|
|
|
118
119
|
session_messages_registry = create_session_messages_registry(
|
|
119
120
|
message_manager=message_manager,
|
|
@@ -150,20 +151,41 @@ def setup_internal_registries(
|
|
|
150
151
|
if metrics_manager is not None:
|
|
151
152
|
from gobby.mcp_proxy.tools.metrics import create_metrics_registry
|
|
152
153
|
|
|
154
|
+
# Get daily budget from conductor config if available
|
|
155
|
+
daily_budget_usd = 50.0 # Default
|
|
156
|
+
if _config is not None:
|
|
157
|
+
conductor_config = _config.conductor
|
|
158
|
+
if conductor_config is not None:
|
|
159
|
+
daily_budget_usd = conductor_config.daily_budget_usd
|
|
160
|
+
|
|
153
161
|
metrics_registry = create_metrics_registry(
|
|
154
162
|
metrics_manager=metrics_manager,
|
|
163
|
+
session_storage=local_session_manager,
|
|
164
|
+
daily_budget_usd=daily_budget_usd,
|
|
155
165
|
)
|
|
156
166
|
manager.add_registry(metrics_registry)
|
|
157
|
-
logger.debug("Metrics registry initialized")
|
|
167
|
+
logger.debug("Metrics registry initialized with token tracking")
|
|
158
168
|
|
|
159
169
|
# Initialize agents registry if agent_runner is available
|
|
160
170
|
if agent_runner is not None:
|
|
171
|
+
from gobby.agents.registry import get_running_agent_registry
|
|
161
172
|
from gobby.mcp_proxy.tools.agents import create_agents_registry
|
|
162
173
|
|
|
163
174
|
agents_registry = create_agents_registry(
|
|
164
175
|
runner=agent_runner,
|
|
165
|
-
tool_proxy_getter=tool_proxy_getter,
|
|
166
176
|
)
|
|
177
|
+
|
|
178
|
+
# Add inter-agent messaging tools if message manager is available
|
|
179
|
+
if inter_session_message_manager is not None:
|
|
180
|
+
from gobby.mcp_proxy.tools.agent_messaging import add_messaging_tools
|
|
181
|
+
|
|
182
|
+
add_messaging_tools(
|
|
183
|
+
registry=agents_registry,
|
|
184
|
+
message_manager=inter_session_message_manager,
|
|
185
|
+
agent_registry=get_running_agent_registry(),
|
|
186
|
+
)
|
|
187
|
+
logger.debug("Agent messaging tools added to agents registry")
|
|
188
|
+
|
|
167
189
|
manager.add_registry(agents_registry)
|
|
168
190
|
logger.debug("Agents registry initialized")
|
|
169
191
|
|
|
@@ -175,11 +197,35 @@ def setup_internal_registries(
|
|
|
175
197
|
worktree_storage=worktree_storage,
|
|
176
198
|
git_manager=git_manager,
|
|
177
199
|
project_id=project_id,
|
|
178
|
-
agent_runner=agent_runner,
|
|
179
200
|
)
|
|
180
201
|
manager.add_registry(worktrees_registry)
|
|
181
202
|
logger.debug("Worktrees registry initialized")
|
|
182
203
|
|
|
204
|
+
# Initialize clones registry if clone_storage is available
|
|
205
|
+
if clone_storage is not None:
|
|
206
|
+
from gobby.clones.git import CloneGitManager
|
|
207
|
+
from gobby.mcp_proxy.tools.clones import create_clones_registry
|
|
208
|
+
|
|
209
|
+
# Create CloneGitManager from the same repo path as WorktreeGitManager
|
|
210
|
+
clone_git_manager = None
|
|
211
|
+
if git_manager is not None:
|
|
212
|
+
try:
|
|
213
|
+
clone_git_manager = CloneGitManager(git_manager.repo_path)
|
|
214
|
+
except Exception as e:
|
|
215
|
+
logger.warning(f"Failed to create CloneGitManager: {e}")
|
|
216
|
+
|
|
217
|
+
# Only create clones registry if we have a git manager
|
|
218
|
+
if clone_git_manager is not None:
|
|
219
|
+
clones_registry = create_clones_registry(
|
|
220
|
+
clone_storage=clone_storage,
|
|
221
|
+
git_manager=clone_git_manager,
|
|
222
|
+
project_id=project_id or "",
|
|
223
|
+
)
|
|
224
|
+
manager.add_registry(clones_registry)
|
|
225
|
+
logger.debug("Clones registry initialized")
|
|
226
|
+
else:
|
|
227
|
+
logger.debug("Clones registry not initialized: CloneGitManager not available")
|
|
228
|
+
|
|
183
229
|
# Initialize merge resolution registry if merge components are available
|
|
184
230
|
if merge_storage is not None and merge_resolver is not None:
|
|
185
231
|
from gobby.mcp_proxy.tools.merge import create_merge_registry
|
|
@@ -204,6 +250,20 @@ def setup_internal_registries(
|
|
|
204
250
|
manager.add_registry(hub_registry)
|
|
205
251
|
logger.debug("Hub registry initialized")
|
|
206
252
|
|
|
253
|
+
# Initialize skills registry using the existing database from task_manager
|
|
254
|
+
# to avoid creating a duplicate connection that would leak
|
|
255
|
+
if task_manager is not None:
|
|
256
|
+
from gobby.mcp_proxy.tools.skills import create_skills_registry
|
|
257
|
+
|
|
258
|
+
skills_registry = create_skills_registry(
|
|
259
|
+
db=task_manager.db,
|
|
260
|
+
project_id=project_id,
|
|
261
|
+
)
|
|
262
|
+
manager.add_registry(skills_registry)
|
|
263
|
+
logger.debug("Skills registry initialized")
|
|
264
|
+
else:
|
|
265
|
+
logger.debug("Skills registry not initialized: task_manager is None")
|
|
266
|
+
|
|
207
267
|
logger.info(f"Internal registries initialized: {len(manager)} registries")
|
|
208
268
|
return manager
|
|
209
269
|
|
gobby/mcp_proxy/server.py
CHANGED
|
@@ -2,13 +2,16 @@
|
|
|
2
2
|
Gobby Daemon Tools MCP Server.
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
+
import json
|
|
5
6
|
import logging
|
|
6
7
|
from datetime import UTC
|
|
7
8
|
from typing import Any
|
|
8
9
|
|
|
9
10
|
from mcp.server.fastmcp import FastMCP
|
|
11
|
+
from mcp.types import CallToolResult, TextContent
|
|
10
12
|
|
|
11
13
|
from gobby.config.app import DaemonConfig
|
|
14
|
+
from gobby.mcp_proxy.instructions import build_gobby_instructions
|
|
12
15
|
from gobby.mcp_proxy.manager import MCPClientManager
|
|
13
16
|
from gobby.mcp_proxy.services.recommendation import RecommendationService, SearchMode
|
|
14
17
|
from gobby.mcp_proxy.services.server_mgmt import ServerManagementService
|
|
@@ -96,8 +99,35 @@ class GobbyDaemonTools:
|
|
|
96
99
|
tool_name: str,
|
|
97
100
|
arguments: dict[str, Any] | None = None,
|
|
98
101
|
) -> Any:
|
|
99
|
-
"""Call a tool.
|
|
100
|
-
|
|
102
|
+
"""Call a tool.
|
|
103
|
+
|
|
104
|
+
Returns the tool result, or a CallToolResult with isError=True if the
|
|
105
|
+
underlying service indicates an error. This ensures the MCP protocol
|
|
106
|
+
properly signals errors to LLM clients instead of returning error dicts
|
|
107
|
+
as successful responses.
|
|
108
|
+
"""
|
|
109
|
+
result = await self.tool_proxy.call_tool(server_name, tool_name, arguments)
|
|
110
|
+
|
|
111
|
+
# Check if result indicates an error (ToolProxyService returns dict with success: False)
|
|
112
|
+
if isinstance(result, dict) and result.get("success") is False:
|
|
113
|
+
# Build helpful error message with schema hint if available
|
|
114
|
+
error_msg = result.get("error", "Unknown error")
|
|
115
|
+
hint = result.get("hint", "")
|
|
116
|
+
schema = result.get("schema")
|
|
117
|
+
|
|
118
|
+
parts = [f"Error: {error_msg}"]
|
|
119
|
+
if hint:
|
|
120
|
+
parts.append(f"\n{hint}")
|
|
121
|
+
if schema:
|
|
122
|
+
parts.append(f"\nCorrect schema:\n{json.dumps(schema, indent=2)}")
|
|
123
|
+
|
|
124
|
+
# Return MCP error response with isError=True
|
|
125
|
+
return CallToolResult(
|
|
126
|
+
content=[TextContent(type="text", text="\n".join(parts))],
|
|
127
|
+
isError=True,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
return result
|
|
101
131
|
|
|
102
132
|
async def list_tools(self, server: str, session_id: str | None = None) -> dict[str, Any]:
|
|
103
133
|
"""List tools for a specific server, optionally filtered by workflow phase restrictions."""
|
|
@@ -513,7 +543,7 @@ class GobbyDaemonTools:
|
|
|
513
543
|
|
|
514
544
|
def create_mcp_server(tools_handler: GobbyDaemonTools) -> FastMCP:
|
|
515
545
|
"""Create the FastMCP server instance for the HTTP daemon."""
|
|
516
|
-
mcp = FastMCP("gobby")
|
|
546
|
+
mcp = FastMCP("gobby", instructions=build_gobby_instructions())
|
|
517
547
|
|
|
518
548
|
# System tools
|
|
519
549
|
mcp.add_tool(tools_handler.status)
|