gobby 0.2.8__py3-none-any.whl → 0.2.11__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.
Files changed (168) hide show
  1. gobby/__init__.py +1 -1
  2. gobby/adapters/__init__.py +6 -0
  3. gobby/adapters/base.py +11 -2
  4. gobby/adapters/claude_code.py +5 -28
  5. gobby/adapters/codex_impl/adapter.py +38 -43
  6. gobby/adapters/copilot.py +324 -0
  7. gobby/adapters/cursor.py +373 -0
  8. gobby/adapters/gemini.py +2 -26
  9. gobby/adapters/windsurf.py +359 -0
  10. gobby/agents/definitions.py +162 -2
  11. gobby/agents/isolation.py +33 -1
  12. gobby/agents/pty_reader.py +192 -0
  13. gobby/agents/registry.py +10 -1
  14. gobby/agents/runner.py +24 -8
  15. gobby/agents/sandbox.py +8 -3
  16. gobby/agents/session.py +4 -0
  17. gobby/agents/spawn.py +9 -2
  18. gobby/agents/spawn_executor.py +49 -61
  19. gobby/agents/spawners/command_builder.py +4 -4
  20. gobby/app_context.py +64 -0
  21. gobby/cli/__init__.py +4 -0
  22. gobby/cli/install.py +259 -4
  23. gobby/cli/installers/__init__.py +12 -0
  24. gobby/cli/installers/copilot.py +242 -0
  25. gobby/cli/installers/cursor.py +244 -0
  26. gobby/cli/installers/shared.py +3 -0
  27. gobby/cli/installers/windsurf.py +242 -0
  28. gobby/cli/pipelines.py +639 -0
  29. gobby/cli/sessions.py +3 -1
  30. gobby/cli/skills.py +209 -0
  31. gobby/cli/tasks/crud.py +6 -5
  32. gobby/cli/tasks/search.py +1 -1
  33. gobby/cli/ui.py +116 -0
  34. gobby/cli/utils.py +5 -17
  35. gobby/cli/workflows.py +38 -17
  36. gobby/config/app.py +5 -0
  37. gobby/config/features.py +0 -20
  38. gobby/config/skills.py +23 -2
  39. gobby/config/tasks.py +4 -0
  40. gobby/hooks/broadcaster.py +9 -0
  41. gobby/hooks/event_handlers/__init__.py +155 -0
  42. gobby/hooks/event_handlers/_agent.py +175 -0
  43. gobby/hooks/event_handlers/_base.py +92 -0
  44. gobby/hooks/event_handlers/_misc.py +66 -0
  45. gobby/hooks/event_handlers/_session.py +487 -0
  46. gobby/hooks/event_handlers/_tool.py +196 -0
  47. gobby/hooks/events.py +48 -0
  48. gobby/hooks/hook_manager.py +27 -3
  49. gobby/install/copilot/hooks/hook_dispatcher.py +203 -0
  50. gobby/install/cursor/hooks/hook_dispatcher.py +203 -0
  51. gobby/install/gemini/hooks/hook_dispatcher.py +8 -0
  52. gobby/install/windsurf/hooks/hook_dispatcher.py +205 -0
  53. gobby/llm/__init__.py +14 -1
  54. gobby/llm/claude.py +594 -43
  55. gobby/llm/service.py +149 -0
  56. gobby/mcp_proxy/importer.py +4 -41
  57. gobby/mcp_proxy/instructions.py +9 -27
  58. gobby/mcp_proxy/manager.py +13 -3
  59. gobby/mcp_proxy/models.py +1 -0
  60. gobby/mcp_proxy/registries.py +66 -5
  61. gobby/mcp_proxy/server.py +6 -2
  62. gobby/mcp_proxy/services/recommendation.py +2 -28
  63. gobby/mcp_proxy/services/tool_filter.py +7 -0
  64. gobby/mcp_proxy/services/tool_proxy.py +19 -1
  65. gobby/mcp_proxy/stdio.py +37 -21
  66. gobby/mcp_proxy/tools/agents.py +7 -0
  67. gobby/mcp_proxy/tools/artifacts.py +3 -3
  68. gobby/mcp_proxy/tools/hub.py +30 -1
  69. gobby/mcp_proxy/tools/orchestration/cleanup.py +5 -5
  70. gobby/mcp_proxy/tools/orchestration/monitor.py +1 -1
  71. gobby/mcp_proxy/tools/orchestration/orchestrate.py +8 -3
  72. gobby/mcp_proxy/tools/orchestration/review.py +17 -4
  73. gobby/mcp_proxy/tools/orchestration/wait.py +7 -7
  74. gobby/mcp_proxy/tools/pipelines/__init__.py +254 -0
  75. gobby/mcp_proxy/tools/pipelines/_discovery.py +67 -0
  76. gobby/mcp_proxy/tools/pipelines/_execution.py +281 -0
  77. gobby/mcp_proxy/tools/sessions/_crud.py +4 -4
  78. gobby/mcp_proxy/tools/sessions/_handoff.py +1 -1
  79. gobby/mcp_proxy/tools/skills/__init__.py +184 -30
  80. gobby/mcp_proxy/tools/spawn_agent.py +229 -14
  81. gobby/mcp_proxy/tools/task_readiness.py +27 -4
  82. gobby/mcp_proxy/tools/tasks/_context.py +8 -0
  83. gobby/mcp_proxy/tools/tasks/_crud.py +27 -1
  84. gobby/mcp_proxy/tools/tasks/_helpers.py +1 -1
  85. gobby/mcp_proxy/tools/tasks/_lifecycle.py +125 -8
  86. gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +2 -1
  87. gobby/mcp_proxy/tools/tasks/_search.py +1 -1
  88. gobby/mcp_proxy/tools/workflows/__init__.py +273 -0
  89. gobby/mcp_proxy/tools/workflows/_artifacts.py +225 -0
  90. gobby/mcp_proxy/tools/workflows/_import.py +112 -0
  91. gobby/mcp_proxy/tools/workflows/_lifecycle.py +332 -0
  92. gobby/mcp_proxy/tools/workflows/_query.py +226 -0
  93. gobby/mcp_proxy/tools/workflows/_resolution.py +78 -0
  94. gobby/mcp_proxy/tools/workflows/_terminal.py +175 -0
  95. gobby/mcp_proxy/tools/worktrees.py +54 -15
  96. gobby/memory/components/__init__.py +0 -0
  97. gobby/memory/components/ingestion.py +98 -0
  98. gobby/memory/components/search.py +108 -0
  99. gobby/memory/context.py +5 -5
  100. gobby/memory/manager.py +16 -25
  101. gobby/paths.py +51 -0
  102. gobby/prompts/loader.py +1 -35
  103. gobby/runner.py +131 -16
  104. gobby/servers/http.py +193 -150
  105. gobby/servers/routes/__init__.py +2 -0
  106. gobby/servers/routes/admin.py +56 -0
  107. gobby/servers/routes/mcp/endpoints/execution.py +33 -32
  108. gobby/servers/routes/mcp/endpoints/registry.py +8 -8
  109. gobby/servers/routes/mcp/hooks.py +10 -1
  110. gobby/servers/routes/pipelines.py +227 -0
  111. gobby/servers/websocket.py +314 -1
  112. gobby/sessions/analyzer.py +89 -3
  113. gobby/sessions/manager.py +5 -5
  114. gobby/sessions/transcripts/__init__.py +3 -0
  115. gobby/sessions/transcripts/claude.py +5 -0
  116. gobby/sessions/transcripts/codex.py +5 -0
  117. gobby/sessions/transcripts/gemini.py +5 -0
  118. gobby/skills/hubs/__init__.py +25 -0
  119. gobby/skills/hubs/base.py +234 -0
  120. gobby/skills/hubs/claude_plugins.py +328 -0
  121. gobby/skills/hubs/clawdhub.py +289 -0
  122. gobby/skills/hubs/github_collection.py +465 -0
  123. gobby/skills/hubs/manager.py +263 -0
  124. gobby/skills/hubs/skillhub.py +342 -0
  125. gobby/skills/parser.py +23 -0
  126. gobby/skills/sync.py +5 -4
  127. gobby/storage/artifacts.py +19 -0
  128. gobby/storage/memories.py +4 -4
  129. gobby/storage/migrations.py +118 -3
  130. gobby/storage/pipelines.py +367 -0
  131. gobby/storage/sessions.py +23 -4
  132. gobby/storage/skills.py +48 -8
  133. gobby/storage/tasks/_aggregates.py +2 -2
  134. gobby/storage/tasks/_lifecycle.py +4 -4
  135. gobby/storage/tasks/_models.py +7 -1
  136. gobby/storage/tasks/_queries.py +3 -3
  137. gobby/sync/memories.py +4 -3
  138. gobby/tasks/commits.py +48 -17
  139. gobby/tasks/external_validator.py +4 -17
  140. gobby/tasks/validation.py +13 -87
  141. gobby/tools/summarizer.py +18 -51
  142. gobby/utils/status.py +13 -0
  143. gobby/workflows/actions.py +80 -0
  144. gobby/workflows/context_actions.py +265 -27
  145. gobby/workflows/definitions.py +119 -1
  146. gobby/workflows/detection_helpers.py +23 -11
  147. gobby/workflows/enforcement/__init__.py +11 -1
  148. gobby/workflows/enforcement/blocking.py +96 -0
  149. gobby/workflows/enforcement/handlers.py +35 -1
  150. gobby/workflows/enforcement/task_policy.py +18 -0
  151. gobby/workflows/engine.py +26 -4
  152. gobby/workflows/evaluator.py +8 -5
  153. gobby/workflows/lifecycle_evaluator.py +59 -27
  154. gobby/workflows/loader.py +567 -30
  155. gobby/workflows/lobster_compat.py +147 -0
  156. gobby/workflows/pipeline_executor.py +801 -0
  157. gobby/workflows/pipeline_state.py +172 -0
  158. gobby/workflows/pipeline_webhooks.py +206 -0
  159. gobby/workflows/premature_stop.py +5 -0
  160. gobby/worktrees/git.py +135 -20
  161. {gobby-0.2.8.dist-info → gobby-0.2.11.dist-info}/METADATA +56 -22
  162. {gobby-0.2.8.dist-info → gobby-0.2.11.dist-info}/RECORD +166 -122
  163. gobby/hooks/event_handlers.py +0 -1008
  164. gobby/mcp_proxy/tools/workflows.py +0 -1023
  165. {gobby-0.2.8.dist-info → gobby-0.2.11.dist-info}/WHEEL +0 -0
  166. {gobby-0.2.8.dist-info → gobby-0.2.11.dist-info}/entry_points.txt +0 -0
  167. {gobby-0.2.8.dist-info → gobby-0.2.11.dist-info}/licenses/LICENSE.md +0 -0
  168. {gobby-0.2.8.dist-info → gobby-0.2.11.dist-info}/top_level.txt +0 -0
gobby/llm/service.py CHANGED
@@ -6,7 +6,9 @@ Gemini, LiteLLM) based on the multi-provider config structure with feature-speci
6
6
  provider routing.
7
7
  """
8
8
 
9
+ import asyncio
9
10
  import logging
11
+ from collections.abc import AsyncIterator
10
12
  from typing import TYPE_CHECKING, Any
11
13
 
12
14
  if TYPE_CHECKING:
@@ -14,6 +16,7 @@ if TYPE_CHECKING:
14
16
  DaemonConfig,
15
17
  )
16
18
  from gobby.llm.base import LLMProvider
19
+ from gobby.llm.claude import ChatEvent
17
20
 
18
21
  logger = logging.getLogger(__name__)
19
22
 
@@ -234,3 +237,149 @@ class LLMService:
234
237
  enabled = self.enabled_providers
235
238
  initialized = self.initialized_providers
236
239
  return f"LLMService(enabled={enabled}, initialized={initialized})"
240
+
241
+ async def stream_chat(
242
+ self,
243
+ messages: list[dict[str, str]],
244
+ provider_name: str | None = None,
245
+ model: str | None = None,
246
+ ) -> AsyncIterator[str]:
247
+ """
248
+ Stream a chat response from the LLM.
249
+
250
+ Takes messages in OpenAI-style format and yields response chunks.
251
+ Currently simulates streaming by chunking the full response.
252
+ Real streaming support can be added per-provider later.
253
+
254
+ Args:
255
+ messages: List of message dicts with 'role' and 'content' keys
256
+ provider_name: Optional provider to use (defaults to default provider)
257
+ model: Optional model override
258
+
259
+ Yields:
260
+ String chunks of the response
261
+
262
+ Example:
263
+ messages = [
264
+ {"role": "system", "content": "You are helpful."},
265
+ {"role": "user", "content": "Hello!"}
266
+ ]
267
+ async for chunk in service.stream_chat(messages):
268
+ print(chunk, end="", flush=True)
269
+ """
270
+ # Get provider
271
+ if provider_name:
272
+ provider = self.get_provider(provider_name)
273
+ else:
274
+ provider = self.get_default_provider()
275
+
276
+ # Build prompt from messages
277
+ system_prompt = None
278
+ user_messages = []
279
+
280
+ for msg in messages:
281
+ role = msg.get("role", "user")
282
+ content = msg.get("content", "")
283
+
284
+ if role == "system":
285
+ system_prompt = content
286
+ else:
287
+ prefix = "User: " if role == "user" else "Assistant: "
288
+ user_messages.append(f"{prefix}{content}")
289
+
290
+ prompt = "\n\n".join(user_messages)
291
+ if user_messages:
292
+ prompt += "\n\nAssistant:"
293
+
294
+ # Generate full response
295
+ response = await provider.generate_text(
296
+ prompt=prompt,
297
+ system_prompt=system_prompt,
298
+ model=model,
299
+ )
300
+
301
+ # Simulate streaming by yielding words with small delays
302
+ # This provides a better UX while we add real streaming later
303
+ words = response.split(" ")
304
+ for i, word in enumerate(words):
305
+ if i > 0:
306
+ yield " "
307
+ yield word
308
+ # Small delay to simulate streaming (5-15ms per word)
309
+ await asyncio.sleep(0.008)
310
+
311
+ async def stream_chat_with_tools(
312
+ self,
313
+ messages: list[dict[str, str]],
314
+ allowed_tools: list[str],
315
+ model: str | None = None,
316
+ max_turns: int = 10,
317
+ ) -> AsyncIterator["ChatEvent"]:
318
+ """
319
+ Stream a chat response with MCP tool support.
320
+
321
+ Takes messages in OpenAI-style format and streams response events
322
+ including text chunks and tool call/result events.
323
+
324
+ This method uses the Claude provider's stream_with_mcp_tools(),
325
+ which requires subscription mode (Claude Agent SDK).
326
+
327
+ Args:
328
+ messages: List of message dicts with 'role' and 'content' keys
329
+ allowed_tools: List of allowed MCP tool patterns.
330
+ Tools should be in format "mcp__{server}__{tool}" or patterns
331
+ like "mcp__gobby-tasks__*" for all tools from a server.
332
+ model: Optional model override
333
+ max_turns: Maximum number of agentic turns (default: 10)
334
+
335
+ Yields:
336
+ ChatEvent: One of TextChunk, ToolCallEvent, ToolResultEvent, or DoneEvent.
337
+
338
+ Example:
339
+ >>> allowed_tools = ["mcp__gobby-tasks__*", "mcp__gobby-memory__*"]
340
+ >>> async for event in service.stream_chat_with_tools(messages, allowed_tools):
341
+ ... if isinstance(event, TextChunk):
342
+ ... print(event.content, end="")
343
+ """
344
+ from gobby.llm.claude import ClaudeLLMProvider, DoneEvent, TextChunk
345
+
346
+ # Get Claude provider (required for MCP tools)
347
+ try:
348
+ provider = self.get_provider("claude")
349
+ except ValueError:
350
+ yield TextChunk(content="Claude provider not configured. MCP tools require Claude.")
351
+ yield DoneEvent(tool_calls_count=0)
352
+ return
353
+
354
+ if not isinstance(provider, ClaudeLLMProvider):
355
+ yield TextChunk(content="MCP tools require Claude provider.")
356
+ yield DoneEvent(tool_calls_count=0)
357
+ return
358
+
359
+ # Build system prompt and user prompt from messages
360
+ system_prompt = None
361
+ user_messages = []
362
+
363
+ for msg in messages:
364
+ role = msg.get("role", "user")
365
+ content = msg.get("content", "")
366
+
367
+ if role == "system":
368
+ system_prompt = content
369
+ else:
370
+ prefix = "User: " if role == "user" else "Assistant: "
371
+ user_messages.append(f"{prefix}{content}")
372
+
373
+ prompt = "\n\n".join(user_messages)
374
+ if user_messages:
375
+ prompt += "\n\nAssistant:"
376
+
377
+ # Stream with MCP tools
378
+ async for event in provider.stream_with_mcp_tools(
379
+ prompt=prompt,
380
+ allowed_tools=allowed_tools,
381
+ system_prompt=system_prompt,
382
+ model=model,
383
+ max_turns=max_turns,
384
+ ):
385
+ yield event
@@ -5,7 +5,6 @@ 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
8
  from gobby.prompts import PromptLoader
10
9
  from gobby.storage.database import DatabaseProtocol
11
10
  from gobby.storage.mcp import LocalMCPManager
@@ -20,21 +19,6 @@ logger = logging.getLogger(__name__)
20
19
  # Pattern to detect placeholder secrets like <YOUR_API_KEY>
21
20
  SECRET_PLACEHOLDER_PATTERN = re.compile(r"<YOUR_[A-Z0-9_]+>")
22
21
 
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
-
38
22
 
39
23
  class MCPServerImporter:
40
24
  """Handles importing MCP servers from various sources."""
@@ -73,11 +57,6 @@ class MCPServerImporter:
73
57
 
74
58
  self._loader = PromptLoader(project_dir=Path(project_path) if project_path else None)
75
59
 
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
-
81
60
  async def import_from_project(
82
61
  self,
83
62
  source_project: str,
@@ -204,19 +183,11 @@ class MCPServerImporter:
204
183
 
205
184
  # Build prompt to fetch and extract config
206
185
  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)
186
+ prompt = self._loader.render(prompt_path, {"github_url": github_url})
212
187
 
213
188
  # Get system prompt
214
189
  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
190
+ system_prompt = self._loader.render(sys_prompt_path, {})
220
191
 
221
192
  options = ClaudeAgentOptions(
222
193
  system_prompt=system_prompt,
@@ -268,19 +239,11 @@ class MCPServerImporter:
268
239
 
269
240
  # Build prompt to search and extract config
270
241
  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)
242
+ prompt = self._loader.render(prompt_path, {"search_query": search_query})
276
243
 
277
244
  # Get system prompt
278
245
  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
246
+ system_prompt = self._loader.render(sys_prompt_path, {})
284
247
 
285
248
  options = ClaudeAgentOptions(
286
249
  system_prompt=system_prompt,
@@ -6,33 +6,16 @@ These instructions are injected into the MCP server via FastMCP's `instructions`
6
6
 
7
7
 
8
8
  def build_gobby_instructions() -> str:
9
- """Build XML-structured instructions for Gobby MCP server.
9
+ """Build compact instructions for Gobby MCP server.
10
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
11
+ Provides minimal guidance for progressive tool disclosure, caching, and task rules.
12
+ Startup sequence and skill discovery are now handled via workflow injection.
19
13
 
20
14
  Returns:
21
- XML-structured instructions string
15
+ XML-structured instructions string (~120 tokens)
22
16
  """
23
17
  return """<gobby_system>
24
18
 
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 `Gobby Session Ref:` or `Gobby Session ID:` in your context.
30
- If missing, call:
31
- `call_tool("gobby-sessions", "get_current_session", {"external_id": "<your-session-id>", "source": "<cli-name>"})`
32
-
33
- Session and task references use `#N` format (e.g., `#1`, `#42`) which is project-scoped.
34
- </startup>
35
-
36
19
  <tool_discovery>
37
20
  NEVER assume tool schemas. Use progressive disclosure:
38
21
  1. `list_tools(server="...")` — Lightweight metadata (~100 tokens/tool)
@@ -40,12 +23,11 @@ NEVER assume tool schemas. Use progressive disclosure:
40
23
  3. `call_tool(server, tool, args)` — Execute
41
24
  </tool_discovery>
42
25
 
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>
26
+ <caching>
27
+ Schema fetches are cached per session. Once you call `get_tool_schema(server, tool)`,
28
+ you can `call_tool` that same server:tool repeatedly WITHOUT re-fetching the schema.
29
+ Do NOT call list_tools or get_tool_schema before every call_tool only on first use.
30
+ </caching>
49
31
 
50
32
  <rules>
51
33
  - Create/claim a task before using Edit, Write, or NotebookEdit tools
@@ -684,6 +684,12 @@ class MCPClientManager:
684
684
 
685
685
  async def get_tool_input_schema(self, server_name: str, tool_name: str) -> dict[str, Any]:
686
686
  """Get full inputSchema for a specific tool."""
687
+ tool_info = await self.get_tool_info(server_name, tool_name)
688
+ input_schema = tool_info.get("inputSchema", {})
689
+ return cast(dict[str, Any], input_schema)
690
+
691
+ async def get_tool_info(self, server_name: str, tool_name: str) -> dict[str, Any]:
692
+ """Get full tool info including name, description, and inputSchema."""
687
693
 
688
694
  # This is an optimization. Instead of calling list_tools again,
689
695
  # we try to fetch it. But standard MCP list_tools returns everything.
@@ -696,9 +702,13 @@ class MCPClientManager:
696
702
  # tool might be an object or dict
697
703
  t_name = getattr(tool, "name", tool.get("name") if isinstance(tool, dict) else None)
698
704
  if t_name == tool_name:
699
- # Return schema
700
- if isinstance(tool, dict) and "inputSchema" in tool:
701
- return cast(dict[str, Any], tool["inputSchema"])
705
+ if isinstance(tool, dict):
706
+ result: dict[str, Any] = {"name": t_name}
707
+ if "description" in tool and tool["description"]:
708
+ result["description"] = tool["description"]
709
+ if "inputSchema" in tool:
710
+ result["inputSchema"] = tool["inputSchema"]
711
+ return result
702
712
 
703
713
  raise MCPError(f"Tool {tool_name} not found on server {server_name}")
704
714
 
gobby/mcp_proxy/models.py CHANGED
@@ -41,6 +41,7 @@ class ToolProxyErrorCode(str, Enum):
41
41
  SERVER_NOT_FOUND = "SERVER_NOT_FOUND"
42
42
  SERVER_NOT_CONFIGURED = "SERVER_NOT_CONFIGURED"
43
43
  TOOL_NOT_FOUND = "TOOL_NOT_FOUND"
44
+ TOOL_BLOCKED = "TOOL_BLOCKED"
44
45
  INVALID_ARGUMENTS = "INVALID_ARGUMENTS"
45
46
  EXECUTION_ERROR = "EXECUTION_ERROR"
46
47
  CONNECTION_ERROR = "CONNECTION_ERROR"
@@ -17,14 +17,18 @@ if TYPE_CHECKING:
17
17
  from gobby.memory.manager import MemoryManager
18
18
  from gobby.sessions.manager import SessionManager
19
19
  from gobby.storage.clones import LocalCloneManager
20
+ from gobby.storage.database import DatabaseProtocol
20
21
  from gobby.storage.inter_session_messages import InterSessionMessageManager
21
22
  from gobby.storage.merge_resolutions import MergeResolutionManager
23
+ from gobby.storage.pipelines import LocalPipelineExecutionManager
22
24
  from gobby.storage.session_messages import LocalSessionMessageManager
23
25
  from gobby.storage.sessions import LocalSessionManager
24
26
  from gobby.storage.tasks import LocalTaskManager
25
27
  from gobby.storage.worktrees import LocalWorktreeManager
26
28
  from gobby.sync.tasks import TaskSyncManager
27
29
  from gobby.tasks.validation import TaskValidator
30
+ from gobby.workflows.loader import WorkflowLoader
31
+ from gobby.workflows.pipeline_executor import PipelineExecutor
28
32
  from gobby.worktrees.git import WorktreeGitManager
29
33
  from gobby.worktrees.merge import MergeResolver
30
34
 
@@ -36,6 +40,7 @@ def setup_internal_registries(
36
40
  _session_manager: SessionManager | None = None,
37
41
  memory_manager: MemoryManager | None = None,
38
42
  task_manager: LocalTaskManager | None = None,
43
+ db: DatabaseProtocol | None = None,
39
44
  sync_manager: TaskSyncManager | None = None,
40
45
  task_validator: TaskValidator | None = None,
41
46
  message_manager: LocalSessionMessageManager | None = None,
@@ -51,6 +56,9 @@ def setup_internal_registries(
51
56
  project_id: str | None = None,
52
57
  tool_proxy_getter: Callable[[], ToolProxyService | None] | None = None,
53
58
  inter_session_message_manager: InterSessionMessageManager | None = None,
59
+ pipeline_executor: PipelineExecutor | None = None,
60
+ workflow_loader: WorkflowLoader | None = None,
61
+ pipeline_execution_manager: LocalPipelineExecutionManager | None = None,
54
62
  ) -> InternalRegistryManager:
55
63
  """
56
64
  Setup internal MCP registries (tasks, messages, memory, metrics, agents, worktrees).
@@ -60,6 +68,7 @@ def setup_internal_registries(
60
68
  _session_manager: Session manager (reserved for future use)
61
69
  memory_manager: Memory manager for memory operations
62
70
  task_manager: Task storage manager
71
+ db: Database connection for registries that only need storage (skills, artifacts)
63
72
  sync_manager: Task sync manager for git sync
64
73
  task_validator: Task validator for validation
65
74
  message_manager: Message storage manager
@@ -75,6 +84,9 @@ def setup_internal_registries(
75
84
  tool_proxy_getter: Callable that returns ToolProxyService for routing
76
85
  tool calls in in-process agents. Called lazily during agent execution.
77
86
  inter_session_message_manager: Inter-session message manager for agent messaging
87
+ pipeline_executor: Pipeline executor for running pipelines
88
+ workflow_loader: Workflow loader for loading pipeline definitions
89
+ pipeline_execution_manager: Pipeline execution manager for tracking executions
78
90
 
79
91
  Returns:
80
92
  InternalRegistryManager containing all registries
@@ -143,6 +155,7 @@ def setup_internal_registries(
143
155
 
144
156
  workflows_registry = create_workflows_registry(
145
157
  session_manager=local_session_manager,
158
+ db=getattr(local_session_manager, "db", None) if local_session_manager else None,
146
159
  )
147
160
  manager.add_registry(workflows_registry)
148
161
  logger.debug("Workflows registry initialized")
@@ -190,6 +203,9 @@ def setup_internal_registries(
190
203
  git_manager=git_manager,
191
204
  clone_storage=clone_storage,
192
205
  clone_manager=clone_git_manager,
206
+ # For mode=self (workflow activation on caller session)
207
+ workflow_loader=workflow_loader,
208
+ db=db,
193
209
  )
194
210
 
195
211
  # Add inter-agent messaging tools if message manager and session manager are available
@@ -267,19 +283,64 @@ def setup_internal_registries(
267
283
  manager.add_registry(hub_registry)
268
284
  logger.debug("Hub registry initialized")
269
285
 
270
- # Initialize skills registry using the existing database from task_manager
271
- # to avoid creating a duplicate connection that would leak
272
- if task_manager is not None:
286
+ # Initialize skills registry if database is available
287
+ if db is not None:
288
+ from gobby.config.skills import SkillsConfig
273
289
  from gobby.mcp_proxy.tools.skills import create_skills_registry
290
+ from gobby.skills.hubs import (
291
+ ClaudePluginsProvider,
292
+ ClawdHubProvider,
293
+ GitHubCollectionProvider,
294
+ HubManager,
295
+ SkillHubProvider,
296
+ )
297
+
298
+ # Get skills config (or use defaults)
299
+ skills_config = _config.skills if _config and hasattr(_config, "skills") else SkillsConfig()
300
+
301
+ # Create hub manager with configured hubs
302
+ hub_manager = HubManager(configs=skills_config.hubs)
303
+
304
+ # Register provider factories
305
+ hub_manager.register_provider_factory("clawdhub", ClawdHubProvider)
306
+ hub_manager.register_provider_factory("skillhub", SkillHubProvider)
307
+ hub_manager.register_provider_factory("github-collection", GitHubCollectionProvider)
308
+ hub_manager.register_provider_factory("claude-plugins", ClaudePluginsProvider)
274
309
 
275
310
  skills_registry = create_skills_registry(
276
- db=task_manager.db,
311
+ db=db,
277
312
  project_id=project_id,
313
+ hub_manager=hub_manager,
278
314
  )
279
315
  manager.add_registry(skills_registry)
280
316
  logger.debug("Skills registry initialized")
281
317
  else:
282
- logger.debug("Skills registry not initialized: task_manager is None")
318
+ logger.debug("Skills registry not initialized: db is None")
319
+
320
+ # Initialize artifacts registry if database is available
321
+ if db is not None:
322
+ from gobby.mcp_proxy.tools.artifacts import create_artifacts_registry
323
+
324
+ artifacts_registry = create_artifacts_registry(
325
+ db=db,
326
+ session_manager=local_session_manager,
327
+ )
328
+ manager.add_registry(artifacts_registry)
329
+ logger.debug("Artifacts registry initialized")
330
+ else:
331
+ logger.debug("Artifacts registry not initialized: db is None")
332
+
333
+ # Initialize pipelines registry if pipeline_executor is available
334
+ if pipeline_executor is not None:
335
+ from gobby.mcp_proxy.tools.pipelines import create_pipelines_registry
336
+
337
+ pipelines_registry = create_pipelines_registry(
338
+ loader=workflow_loader,
339
+ executor=pipeline_executor,
340
+ execution_manager=pipeline_execution_manager,
341
+ )
342
+ manager.add_registry(pipelines_registry)
343
+ logger.debug("Pipelines registry initialized")
283
344
 
284
345
  logger.info(f"Internal registries initialized: {len(manager)} registries")
285
346
  return manager
gobby/mcp_proxy/server.py CHANGED
@@ -98,6 +98,7 @@ class GobbyDaemonTools:
98
98
  server_name: str,
99
99
  tool_name: str,
100
100
  arguments: dict[str, Any] | None = None,
101
+ session_id: str | None = None,
101
102
  ) -> Any:
102
103
  """Call a tool.
103
104
 
@@ -105,8 +106,11 @@ class GobbyDaemonTools:
105
106
  underlying service indicates an error. This ensures the MCP protocol
106
107
  properly signals errors to LLM clients instead of returning error dicts
107
108
  as successful responses.
109
+
110
+ When session_id is provided and a workflow is active, checks that the
111
+ tool is not blocked by the current workflow step's blocked_tools setting.
108
112
  """
109
- result = await self.tool_proxy.call_tool(server_name, tool_name, arguments)
113
+ result = await self.tool_proxy.call_tool(server_name, tool_name, arguments, session_id)
110
114
 
111
115
  # Check if result indicates an error (ToolProxyService returns dict with success: False)
112
116
  if isinstance(result, dict) and result.get("success") is False:
@@ -382,7 +386,7 @@ class GobbyDaemonTools:
382
386
 
383
387
  Args:
384
388
  event_type: Hook event type (e.g., "session_start", "before_tool")
385
- source: Source CLI to simulate (claude, gemini, codex)
389
+ source: Source CLI to simulate (claude, gemini, codex, cursor, windsurf, copilot)
386
390
  data: Optional additional data for the event
387
391
 
388
392
  Returns:
@@ -15,22 +15,6 @@ logger = logging.getLogger("gobby.mcp.server")
15
15
  # Search mode type
16
16
  SearchMode = Literal["llm", "semantic", "hybrid"]
17
17
 
18
- DEFAULT_HYBRID_RERANK_PROMPT = """Re-rank the following tools for the task: "{task_description}"
19
-
20
- Candidates:
21
- {candidate_list}
22
-
23
- Select the best {top_k} tools. Return JSON:
24
- {{"recommendations": [{{"server": "...", "tool": "...", "reason": "..."}}]}}"""
25
-
26
- DEFAULT_LLM_PROMPT = """Recommend tools for the task: "{task_description}"
27
-
28
- Available Servers:
29
- {available_servers}
30
-
31
- Return JSON:
32
- {{"recommendations": [{{"server": "...", "tool": "...", "reason": "..."}}]}}"""
33
-
34
18
 
35
19
  class RecommendationService:
36
20
  """Service for recommending tools."""
@@ -49,10 +33,6 @@ class RecommendationService:
49
33
  self._project_id = project_id
50
34
  self._config = config
51
35
  self._loader = PromptLoader()
52
- self._loader.register_fallback(
53
- "features/recommend_hybrid", lambda: DEFAULT_HYBRID_RERANK_PROMPT
54
- )
55
- self._loader.register_fallback("features/recommend_llm", lambda: DEFAULT_LLM_PROMPT)
56
36
 
57
37
  def _get_config(self) -> RecommendToolsConfig:
58
38
  """Get config with fallback to defaults."""
@@ -181,10 +161,7 @@ class RecommendationService:
181
161
  "candidate_list": candidate_list,
182
162
  "top_k": top_k,
183
163
  }
184
- try:
185
- prompt = self._loader.render(prompt_path, context)
186
- except Exception:
187
- prompt = DEFAULT_HYBRID_RERANK_PROMPT.format(**context)
164
+ prompt = self._loader.render(prompt_path, context)
188
165
 
189
166
  provider = self._llm_service.get_default_provider()
190
167
  response = await provider.generate_text(prompt)
@@ -223,10 +200,7 @@ class RecommendationService:
223
200
  "task_description": task_description,
224
201
  "available_servers": ", ".join(available_servers),
225
202
  }
226
- try:
227
- prompt = self._loader.render(prompt_path, context)
228
- except Exception:
229
- prompt = DEFAULT_LLM_PROMPT.format(**context)
203
+ prompt = self._loader.render(prompt_path, context)
230
204
 
231
205
  provider = self._llm_service.get_default_provider()
232
206
  response = await provider.generate_text(prompt)
@@ -4,6 +4,8 @@ import logging
4
4
  from pathlib import Path
5
5
  from typing import TYPE_CHECKING, Any
6
6
 
7
+ from gobby.workflows.definitions import WorkflowDefinition
8
+
7
9
  if TYPE_CHECKING:
8
10
  from gobby.storage.database import LocalDatabase
9
11
  from gobby.workflows.loader import WorkflowLoader
@@ -89,6 +91,11 @@ class ToolFilterService:
89
91
  logger.warning(f"Workflow '{state.workflow_name}' not found")
90
92
  return None
91
93
 
94
+ # Tool filtering only applies to step-based workflows
95
+ if not isinstance(definition, WorkflowDefinition):
96
+ logger.debug(f"Workflow '{state.workflow_name}' is not a step-based workflow")
97
+ return None
98
+
92
99
  step = definition.get_step(state.step)
93
100
  if not step:
94
101
  logger.warning(f"Step '{state.step}' not found in workflow '{state.workflow_name}'")
@@ -208,6 +208,7 @@ class ToolProxyService:
208
208
  server_name: str,
209
209
  tool_name: str,
210
210
  arguments: dict[str, Any] | None = None,
211
+ session_id: str | None = None,
211
212
  ) -> Any:
212
213
  """Execute a tool with optional pre-validation.
213
214
 
@@ -218,9 +219,24 @@ class ToolProxyService:
218
219
  On execution error, includes fallback_suggestions if a fallback resolver
219
220
  is configured.
220
221
 
222
+ When session_id is provided and a workflow is active, checks that the
223
+ tool is not blocked by the current workflow step's blocked_tools setting.
224
+
221
225
  """
222
226
  args = arguments or {}
223
227
 
228
+ # Check workflow tool restrictions if session_id provided
229
+ if session_id and self._tool_filter:
230
+ is_allowed, reason = self._tool_filter.is_tool_allowed(tool_name, session_id)
231
+ if not is_allowed:
232
+ return {
233
+ "success": False,
234
+ "error": reason,
235
+ "error_code": ToolProxyErrorCode.TOOL_BLOCKED.value,
236
+ "server_name": server_name,
237
+ "tool_name": tool_name,
238
+ }
239
+
224
240
  # Pre-validate arguments if enabled
225
241
  if self._validate_arguments and args:
226
242
  schema_result = await self.get_tool_schema(server_name, tool_name)
@@ -361,6 +377,7 @@ class ToolProxyService:
361
377
  self,
362
378
  tool_name: str,
363
379
  arguments: dict[str, Any] | None = None,
380
+ session_id: str | None = None,
364
381
  ) -> Any:
365
382
  """
366
383
  Call a tool by name, automatically resolving the server.
@@ -371,6 +388,7 @@ class ToolProxyService:
371
388
  Args:
372
389
  tool_name: Name of the tool to call
373
390
  arguments: Tool arguments
391
+ session_id: Optional session ID for workflow tool restriction checks
374
392
 
375
393
  Returns:
376
394
  Tool execution result, or error dict if tool not found
@@ -386,4 +404,4 @@ class ToolProxyService:
386
404
  }
387
405
 
388
406
  logger.debug(f"Routing tool '{tool_name}' to server '{server_name}'")
389
- return await self.call_tool(server_name, tool_name, arguments)
407
+ return await self.call_tool(server_name, tool_name, arguments, session_id)