ouroboros-ai 0.3.0__py3-none-any.whl → 0.4.0__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.

Potentially problematic release.


This version of ouroboros-ai might be problematic. Click here for more details.

Files changed (42) hide show
  1. ouroboros/__init__.py +1 -1
  2. ouroboros/bigbang/__init__.py +9 -0
  3. ouroboros/bigbang/ontology.py +180 -0
  4. ouroboros/cli/commands/__init__.py +2 -0
  5. ouroboros/cli/commands/mcp.py +161 -0
  6. ouroboros/cli/commands/run.py +165 -27
  7. ouroboros/cli/main.py +2 -1
  8. ouroboros/core/ontology_aspect.py +455 -0
  9. ouroboros/core/ontology_questions.py +462 -0
  10. ouroboros/evaluation/__init__.py +16 -1
  11. ouroboros/evaluation/consensus.py +569 -11
  12. ouroboros/evaluation/models.py +81 -0
  13. ouroboros/events/ontology.py +135 -0
  14. ouroboros/mcp/__init__.py +83 -0
  15. ouroboros/mcp/client/__init__.py +20 -0
  16. ouroboros/mcp/client/adapter.py +632 -0
  17. ouroboros/mcp/client/manager.py +600 -0
  18. ouroboros/mcp/client/protocol.py +161 -0
  19. ouroboros/mcp/errors.py +377 -0
  20. ouroboros/mcp/resources/__init__.py +22 -0
  21. ouroboros/mcp/resources/handlers.py +328 -0
  22. ouroboros/mcp/server/__init__.py +21 -0
  23. ouroboros/mcp/server/adapter.py +408 -0
  24. ouroboros/mcp/server/protocol.py +291 -0
  25. ouroboros/mcp/server/security.py +636 -0
  26. ouroboros/mcp/tools/__init__.py +24 -0
  27. ouroboros/mcp/tools/definitions.py +351 -0
  28. ouroboros/mcp/tools/registry.py +269 -0
  29. ouroboros/mcp/types.py +333 -0
  30. ouroboros/orchestrator/__init__.py +31 -0
  31. ouroboros/orchestrator/events.py +40 -0
  32. ouroboros/orchestrator/mcp_config.py +419 -0
  33. ouroboros/orchestrator/mcp_tools.py +483 -0
  34. ouroboros/orchestrator/runner.py +119 -2
  35. ouroboros/providers/claude_code_adapter.py +75 -0
  36. ouroboros/strategies/__init__.py +23 -0
  37. ouroboros/strategies/devil_advocate.py +197 -0
  38. {ouroboros_ai-0.3.0.dist-info → ouroboros_ai-0.4.0.dist-info}/METADATA +10 -5
  39. {ouroboros_ai-0.3.0.dist-info → ouroboros_ai-0.4.0.dist-info}/RECORD +42 -17
  40. {ouroboros_ai-0.3.0.dist-info → ouroboros_ai-0.4.0.dist-info}/WHEEL +0 -0
  41. {ouroboros_ai-0.3.0.dist-info → ouroboros_ai-0.4.0.dist-info}/entry_points.txt +0 -0
  42. {ouroboros_ai-0.3.0.dist-info → ouroboros_ai-0.4.0.dist-info}/licenses/LICENSE +0 -0
ouroboros/__init__.py CHANGED
@@ -13,7 +13,7 @@ Example:
13
13
  from ouroboros.bigbang import InterviewEngine
14
14
  """
15
15
 
16
- __version__ = "0.2.0"
16
+ __version__ = "0.4.0"
17
17
 
18
18
  __all__ = ["__version__", "main"]
19
19
 
@@ -14,6 +14,11 @@ from ouroboros.bigbang.ambiguity import (
14
14
  is_ready_for_seed,
15
15
  )
16
16
  from ouroboros.bigbang.interview import InterviewEngine, InterviewState
17
+ from ouroboros.bigbang.ontology import (
18
+ InterviewOntologyAnalyzer,
19
+ OntologicalQuestionDecision,
20
+ default_interview_ontology_analyzer,
21
+ )
17
22
  from ouroboros.bigbang.seed_generator import (
18
23
  SeedGenerator,
19
24
  load_seed,
@@ -32,6 +37,10 @@ __all__ = [
32
37
  # Interview
33
38
  "InterviewEngine",
34
39
  "InterviewState",
40
+ # Ontology (for Interview)
41
+ "InterviewOntologyAnalyzer",
42
+ "OntologicalQuestionDecision",
43
+ "default_interview_ontology_analyzer",
35
44
  # Seed Generation
36
45
  "SeedGenerator",
37
46
  "load_seed",
@@ -0,0 +1,180 @@
1
+ """Ontological questioning integration for Interview Phase.
2
+
3
+ This module provides the bridge between the core ontological framework
4
+ and the Interview engine. It determines WHEN and WHICH ontological
5
+ questions should be asked during the interview process.
6
+
7
+ The Two Ancient Methods:
8
+ 1. Socratic Questioning (existing) - "Why?", "What if?"
9
+ → Reveals hidden assumptions through iterative questioning
10
+
11
+ 2. Ontological Analysis (this module) - "What IS this?", "Root cause or symptom?"
12
+ → Finds root problems, ensures we're solving the right thing
13
+
14
+ These methods are interleaved: Socratic questions every round,
15
+ ontological questions periodically to probe deeper.
16
+ """
17
+
18
+ from dataclasses import dataclass
19
+
20
+ from ouroboros.core.ontology_questions import (
21
+ ONTOLOGICAL_QUESTIONS,
22
+ OntologicalQuestionType,
23
+ build_ontological_prompt,
24
+ )
25
+
26
+ # Question schedule: which ontological question type to use at each milestone
27
+ # Pattern: ESSENCE → ROOT_CAUSE → PREREQUISITES → HIDDEN_ASSUMPTIONS
28
+ _QUESTION_SCHEDULE: tuple[OntologicalQuestionType, ...] = (
29
+ OntologicalQuestionType.ESSENCE, # Round 3: What IS this?
30
+ OntologicalQuestionType.ROOT_CAUSE, # Round 6: Root cause or symptom?
31
+ OntologicalQuestionType.PREREQUISITES, # Round 9: What must exist first?
32
+ OntologicalQuestionType.HIDDEN_ASSUMPTIONS, # Round 12+: What are we assuming?
33
+ )
34
+
35
+
36
+ @dataclass(frozen=True, slots=True)
37
+ class OntologicalQuestionDecision:
38
+ """Decision about whether to ask an ontological question.
39
+
40
+ Attributes:
41
+ should_ask: Whether an ontological question should be asked this round.
42
+ question_type: The type of question to ask (if should_ask is True).
43
+ system_prompt_addition: Text to add to system prompt (if should_ask).
44
+ """
45
+
46
+ should_ask: bool
47
+ question_type: OntologicalQuestionType | None = None
48
+ system_prompt_addition: str = ""
49
+
50
+
51
+ class InterviewOntologyAnalyzer:
52
+ """Analyzer that decides when to inject ontological questions.
53
+
54
+ Ontological questions are interspersed with Socratic questioning
55
+ to periodically probe the fundamental nature of the problem.
56
+
57
+ Schedule:
58
+ - Round 1-2: Pure Socratic (building basic context)
59
+ - Round 3: ESSENCE - "What IS this, really?"
60
+ - Round 4-5: Pure Socratic
61
+ - Round 6: ROOT_CAUSE - "Is this root cause or symptom?"
62
+ - Round 7-8: Pure Socratic
63
+ - Round 9: PREREQUISITES - "What must exist first?"
64
+ - Round 10-11: Pure Socratic
65
+ - Round 12+: HIDDEN_ASSUMPTIONS (every 3rd round after)
66
+ """
67
+
68
+ def __init__(self, start_round: int = 3, frequency: int = 3) -> None:
69
+ """Initialize the analyzer.
70
+
71
+ Args:
72
+ start_round: First round to start ontological questioning.
73
+ frequency: Ask ontological questions every N rounds.
74
+ """
75
+ self._start_round = start_round
76
+ self._frequency = frequency
77
+
78
+ def should_ask_ontological_question(self, round_number: int) -> bool:
79
+ """Determine if this round should include an ontological question.
80
+
81
+ Args:
82
+ round_number: Current interview round (1-based).
83
+
84
+ Returns:
85
+ True if ontological question should be asked.
86
+ """
87
+ if round_number < self._start_round:
88
+ return False
89
+ return (round_number - self._start_round) % self._frequency == 0
90
+
91
+ def select_question_type(
92
+ self,
93
+ round_number: int,
94
+ ) -> OntologicalQuestionType:
95
+ """Select which ontological question type to ask.
96
+
97
+ Uses a rotating schedule through the four question types.
98
+
99
+ Args:
100
+ round_number: Current interview round (1-based).
101
+
102
+ Returns:
103
+ The question type to use for this round.
104
+ """
105
+ if round_number < self._start_round:
106
+ # Default to ESSENCE for early rounds (shouldn't happen normally)
107
+ return OntologicalQuestionType.ESSENCE
108
+
109
+ # Calculate position in schedule
110
+ position = (round_number - self._start_round) // self._frequency
111
+ schedule_index = position % len(_QUESTION_SCHEDULE)
112
+ return _QUESTION_SCHEDULE[schedule_index]
113
+
114
+ def get_decision(self, round_number: int) -> OntologicalQuestionDecision:
115
+ """Get the complete decision for a round.
116
+
117
+ Args:
118
+ round_number: Current interview round (1-based).
119
+
120
+ Returns:
121
+ Decision with all necessary information.
122
+ """
123
+ should_ask = self.should_ask_ontological_question(round_number)
124
+
125
+ if not should_ask:
126
+ return OntologicalQuestionDecision(should_ask=False)
127
+
128
+ question_type = self.select_question_type(round_number)
129
+ prompt_addition = build_ontological_prompt(question_type)
130
+
131
+ return OntologicalQuestionDecision(
132
+ should_ask=True,
133
+ question_type=question_type,
134
+ system_prompt_addition=prompt_addition,
135
+ )
136
+
137
+ def build_ontological_system_prompt(
138
+ self,
139
+ round_number: int,
140
+ base_prompt: str,
141
+ ) -> str:
142
+ """Build system prompt with optional ontological addition.
143
+
144
+ If this round requires an ontological question, it's appended
145
+ to the base prompt. Otherwise, the base prompt is returned as-is.
146
+
147
+ Args:
148
+ round_number: Current interview round (1-based).
149
+ base_prompt: The base Socratic system prompt.
150
+
151
+ Returns:
152
+ Enhanced prompt with ontological guidance if applicable.
153
+ """
154
+ decision = self.get_decision(round_number)
155
+
156
+ if not decision.should_ask or decision.question_type is None:
157
+ return base_prompt
158
+
159
+ # Add ontological context
160
+ question = ONTOLOGICAL_QUESTIONS[decision.question_type]
161
+ return f"""{base_prompt}
162
+
163
+ ---
164
+ ONTOLOGICAL FOCUS FOR THIS ROUND:
165
+ {decision.system_prompt_addition}
166
+ Your question this round should probe: {question.question}
167
+ Purpose: {question.purpose}
168
+ Consider: {question.follow_up}
169
+ ---"""
170
+
171
+
172
+ # Default singleton for convenience
173
+ default_interview_ontology_analyzer = InterviewOntologyAnalyzer()
174
+
175
+
176
+ __all__ = [
177
+ "InterviewOntologyAnalyzer",
178
+ "OntologicalQuestionDecision",
179
+ "default_interview_ontology_analyzer",
180
+ ]
@@ -1,7 +1,9 @@
1
1
  """CLI command implementations for Ouroboros.
2
2
 
3
3
  This module contains the command group implementations:
4
+ - init: Start interactive interview
4
5
  - run: Execute workflows
5
6
  - config: Manage configuration
6
7
  - status: Check system status
8
+ - mcp: MCP server management
7
9
  """
@@ -0,0 +1,161 @@
1
+ """MCP command group for Ouroboros.
2
+
3
+ Start and manage the MCP (Model Context Protocol) server.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import asyncio
9
+ from typing import Annotated
10
+
11
+ import typer
12
+
13
+ from ouroboros.cli.formatters.panels import print_error, print_info, print_success
14
+
15
+ app = typer.Typer(
16
+ name="mcp",
17
+ help="MCP (Model Context Protocol) server commands.",
18
+ no_args_is_help=True,
19
+ )
20
+
21
+
22
+ async def _run_mcp_server(
23
+ host: str,
24
+ port: int,
25
+ transport: str,
26
+ ) -> None:
27
+ """Run the MCP server.
28
+
29
+ Args:
30
+ host: Host to bind to.
31
+ port: Port to bind to.
32
+ transport: Transport type (stdio or sse).
33
+ """
34
+ from ouroboros.mcp.server.adapter import create_ouroboros_server
35
+ from ouroboros.mcp.tools.definitions import OUROBOROS_TOOLS
36
+
37
+ # Create server
38
+ server = create_ouroboros_server(
39
+ name="ouroboros-mcp",
40
+ version="1.0.0",
41
+ )
42
+
43
+ # Register tools
44
+ for tool in OUROBOROS_TOOLS:
45
+ server.register_tool(tool)
46
+
47
+ print_success(f"MCP Server starting on {transport}...")
48
+ print_info(f"Registered {len(OUROBOROS_TOOLS)} tools")
49
+
50
+ if transport == "stdio":
51
+ print_info("Reading from stdin, writing to stdout")
52
+ print_info("Press Ctrl+C to stop")
53
+ else:
54
+ print_info(f"Listening on {host}:{port}")
55
+ print_info("Press Ctrl+C to stop")
56
+
57
+ # Start serving
58
+ await server.serve(transport=transport)
59
+
60
+
61
+ @app.command()
62
+ def serve(
63
+ host: Annotated[
64
+ str,
65
+ typer.Option(
66
+ "--host",
67
+ "-h",
68
+ help="Host to bind to.",
69
+ ),
70
+ ] = "localhost",
71
+ port: Annotated[
72
+ int,
73
+ typer.Option(
74
+ "--port",
75
+ "-p",
76
+ help="Port to bind to.",
77
+ ),
78
+ ] = 8080,
79
+ transport: Annotated[
80
+ str,
81
+ typer.Option(
82
+ "--transport",
83
+ "-t",
84
+ help="Transport type: stdio or sse.",
85
+ ),
86
+ ] = "stdio",
87
+ ) -> None:
88
+ """Start the MCP server.
89
+
90
+ Exposes Ouroboros functionality via Model Context Protocol,
91
+ allowing Claude Desktop and other MCP clients to interact
92
+ with Ouroboros.
93
+
94
+ Available tools:
95
+ - ouroboros_execute_seed: Execute a seed specification
96
+ - ouroboros_session_status: Get session status
97
+ - ouroboros_query_events: Query event history
98
+
99
+ Examples:
100
+
101
+ # Start with stdio transport (for Claude Desktop)
102
+ ouroboros mcp serve
103
+
104
+ # Start with SSE transport on custom port
105
+ ouroboros mcp serve --transport sse --port 9000
106
+ """
107
+ try:
108
+ asyncio.run(_run_mcp_server(host, port, transport))
109
+ except KeyboardInterrupt:
110
+ print_info("\nMCP Server stopped")
111
+ except ImportError as e:
112
+ print_error(f"MCP dependencies not installed: {e}")
113
+ print_info("Install with: uv add mcp")
114
+ raise typer.Exit(1) from e
115
+
116
+
117
+ @app.command()
118
+ def info() -> None:
119
+ """Show MCP server information and available tools."""
120
+ from ouroboros.mcp.server.adapter import create_ouroboros_server
121
+ from ouroboros.mcp.tools.definitions import OUROBOROS_TOOLS
122
+
123
+ from ouroboros.cli.formatters import console
124
+
125
+ # Create server
126
+ server = create_ouroboros_server(
127
+ name="ouroboros-mcp",
128
+ version="1.0.0",
129
+ )
130
+
131
+ # Register tools
132
+ for tool in OUROBOROS_TOOLS:
133
+ server.register_tool(tool)
134
+
135
+ server_info = server.info
136
+
137
+ console.print()
138
+ console.print("[bold]MCP Server Information[/bold]")
139
+ console.print(f" Name: {server_info.name}")
140
+ console.print(f" Version: {server_info.version}")
141
+ console.print()
142
+
143
+ console.print("[bold]Capabilities[/bold]")
144
+ console.print(f" Tools: {server_info.capabilities.tools}")
145
+ console.print(f" Resources: {server_info.capabilities.resources}")
146
+ console.print(f" Prompts: {server_info.capabilities.prompts}")
147
+ console.print()
148
+
149
+ console.print("[bold]Available Tools[/bold]")
150
+ for tool in server_info.tools:
151
+ console.print(f" [green]{tool.name}[/green]")
152
+ console.print(f" {tool.description}")
153
+ if tool.parameters:
154
+ console.print(" Parameters:")
155
+ for param in tool.parameters:
156
+ required = "[red]*[/red]" if param.required else ""
157
+ console.print(f" - {param.name}{required}: {param.description}")
158
+ console.print()
159
+
160
+
161
+ __all__ = ["app"]
@@ -8,13 +8,16 @@ from __future__ import annotations
8
8
 
9
9
  import asyncio
10
10
  from pathlib import Path
11
- from typing import Annotated
11
+ from typing import TYPE_CHECKING, Annotated, Any
12
12
 
13
13
  import typer
14
14
  import yaml
15
15
 
16
+ if TYPE_CHECKING:
17
+ from ouroboros.mcp.client.manager import MCPClientManager
18
+
16
19
  from ouroboros.cli.formatters import console
17
- from ouroboros.cli.formatters.panels import print_error, print_info, print_success
20
+ from ouroboros.cli.formatters.panels import print_error, print_info, print_success, print_warning
18
21
  from ouroboros.core.security import InputValidator
19
22
 
20
23
  app = typer.Typer(
@@ -24,7 +27,7 @@ app = typer.Typer(
24
27
  )
25
28
 
26
29
 
27
- def _load_seed_from_yaml(seed_file: Path) -> dict:
30
+ def _load_seed_from_yaml(seed_file: Path) -> dict[str, Any]:
28
31
  """Load seed configuration from YAML file.
29
32
 
30
33
  Args:
@@ -45,21 +48,88 @@ def _load_seed_from_yaml(seed_file: Path) -> dict:
45
48
 
46
49
  try:
47
50
  with open(seed_file) as f:
48
- return yaml.safe_load(f)
51
+ data: dict[str, Any] = yaml.safe_load(f)
52
+ return data
49
53
  except Exception as e:
50
54
  print_error(f"Failed to load seed file: {e}")
51
55
  raise typer.Exit(1) from e
52
56
 
53
57
 
58
+ async def _initialize_mcp_manager(
59
+ config_path: Path,
60
+ tool_prefix: str,
61
+ ) -> "MCPClientManager | None":
62
+ """Initialize MCPClientManager from config file.
63
+
64
+ Args:
65
+ config_path: Path to MCP config YAML.
66
+ tool_prefix: Prefix to add to MCP tool names.
67
+
68
+ Returns:
69
+ Configured MCPClientManager or None on error.
70
+ """
71
+ from ouroboros.mcp.client.manager import MCPClientManager
72
+ from ouroboros.orchestrator.mcp_config import load_mcp_config
73
+
74
+ # Load configuration
75
+ result = load_mcp_config(config_path)
76
+ if result.is_err:
77
+ print_error(f"Failed to load MCP config: {result.error}")
78
+ return None
79
+
80
+ config = result.value
81
+
82
+ # Create manager with connection settings
83
+ manager = MCPClientManager(
84
+ max_retries=config.connection.retry_attempts,
85
+ health_check_interval=config.connection.health_check_interval,
86
+ default_timeout=config.connection.timeout_seconds,
87
+ )
88
+
89
+ # Add all servers
90
+ for server_config in config.servers:
91
+ add_result = await manager.add_server(server_config)
92
+ if add_result.is_err:
93
+ print_warning(f"Failed to add MCP server '{server_config.name}': {add_result.error}")
94
+ else:
95
+ print_info(f"Added MCP server: {server_config.name}")
96
+
97
+ # Connect to all servers
98
+ if manager.servers:
99
+ print_info("Connecting to MCP servers...")
100
+ connect_results = await manager.connect_all()
101
+
102
+ connected_count = 0
103
+ for server_name, connect_result in connect_results.items():
104
+ if connect_result.is_ok:
105
+ server_info = connect_result.value
106
+ print_success(f" Connected to '{server_name}' ({len(server_info.tools)} tools)")
107
+ connected_count += 1
108
+ else:
109
+ print_warning(f" Failed to connect to '{server_name}': {connect_result.error}")
110
+
111
+ if connected_count == 0:
112
+ print_warning("No MCP servers connected. Continuing without external tools.")
113
+ return None
114
+
115
+ print_info(f"Connected to {connected_count}/{len(manager.servers)} MCP servers")
116
+
117
+ return manager
118
+
119
+
54
120
  async def _run_orchestrator(
55
121
  seed_file: Path,
56
122
  resume_session: str | None = None,
123
+ mcp_config: Path | None = None,
124
+ mcp_tool_prefix: str = "",
57
125
  ) -> None:
58
126
  """Run workflow via orchestrator mode (Claude Agent SDK).
59
127
 
60
128
  Args:
61
129
  seed_file: Path to seed YAML file.
62
130
  resume_session: Optional session ID to resume.
131
+ mcp_config: Optional path to MCP config file.
132
+ mcp_tool_prefix: Prefix for MCP tool names.
63
133
  """
64
134
  from ouroboros.core.seed import Seed
65
135
  from ouroboros.orchestrator import ClaudeAgentAdapter, OrchestratorRunner
@@ -77,6 +147,12 @@ async def _run_orchestrator(
77
147
  print_info(f"Loaded seed: {seed.goal[:80]}...")
78
148
  print_info(f"Acceptance criteria: {len(seed.acceptance_criteria)}")
79
149
 
150
+ # Initialize MCP manager if config provided
151
+ mcp_manager = None
152
+ if mcp_config:
153
+ print_info(f"Loading MCP configuration from: {mcp_config}")
154
+ mcp_manager = await _initialize_mcp_manager(mcp_config, mcp_tool_prefix)
155
+
80
156
  # Initialize components
81
157
  import os
82
158
  db_path = os.path.expanduser("~/.ouroboros/ouroboros.db")
@@ -85,32 +161,44 @@ async def _run_orchestrator(
85
161
  await event_store.initialize()
86
162
 
87
163
  adapter = ClaudeAgentAdapter()
88
- runner = OrchestratorRunner(adapter, event_store, console)
164
+ runner = OrchestratorRunner(
165
+ adapter,
166
+ event_store,
167
+ console,
168
+ mcp_manager=mcp_manager,
169
+ mcp_tool_prefix=mcp_tool_prefix,
170
+ )
89
171
 
90
172
  # Execute
91
- if resume_session:
92
- print_info(f"Resuming session: {resume_session}")
93
- result = await runner.resume_session(resume_session, seed)
94
- else:
95
- print_info("Starting new orchestrator execution...")
96
- result = await runner.execute_seed(seed)
97
-
98
- # Handle result
99
- if result.is_ok:
100
- res = result.value
101
- if res.success:
102
- print_success("Execution completed successfully!")
103
- print_info(f"Session ID: {res.session_id}")
104
- print_info(f"Messages processed: {res.messages_processed}")
105
- print_info(f"Duration: {res.duration_seconds:.1f}s")
173
+ try:
174
+ if resume_session:
175
+ print_info(f"Resuming session: {resume_session}")
176
+ result = await runner.resume_session(resume_session, seed)
177
+ else:
178
+ print_info("Starting new orchestrator execution...")
179
+ result = await runner.execute_seed(seed)
180
+
181
+ # Handle result
182
+ if result.is_ok:
183
+ res = result.value
184
+ if res.success:
185
+ print_success("Execution completed successfully!")
186
+ print_info(f"Session ID: {res.session_id}")
187
+ print_info(f"Messages processed: {res.messages_processed}")
188
+ print_info(f"Duration: {res.duration_seconds:.1f}s")
189
+ else:
190
+ print_error("Execution failed")
191
+ print_info(f"Session ID: {res.session_id}")
192
+ console.print(f"[dim]Error: {res.final_message[:200]}[/dim]")
193
+ raise typer.Exit(1)
106
194
  else:
107
- print_error("Execution failed")
108
- print_info(f"Session ID: {res.session_id}")
109
- console.print(f"[dim]Error: {res.final_message[:200]}[/dim]")
195
+ print_error(f"Orchestrator error: {result.error}")
110
196
  raise typer.Exit(1)
111
- else:
112
- print_error(f"Orchestrator error: {result.error}")
113
- raise typer.Exit(1)
197
+ finally:
198
+ # Cleanup MCP connections
199
+ if mcp_manager:
200
+ print_info("Disconnecting MCP servers...")
201
+ await mcp_manager.disconnect_all()
114
202
 
115
203
 
116
204
  @app.command()
@@ -141,6 +229,20 @@ def workflow(
141
229
  help="Resume a previous orchestrator session by ID.",
142
230
  ),
143
231
  ] = None,
232
+ mcp_config: Annotated[
233
+ Path | None,
234
+ typer.Option(
235
+ "--mcp-config",
236
+ help="Path to MCP client configuration YAML file for external tool integration.",
237
+ ),
238
+ ] = None,
239
+ mcp_tool_prefix: Annotated[
240
+ str,
241
+ typer.Option(
242
+ "--mcp-tool-prefix",
243
+ help="Prefix to add to all MCP tool names (e.g., 'mcp_').",
244
+ ),
245
+ ] = "",
144
246
  dry_run: Annotated[
145
247
  bool,
146
248
  typer.Option("--dry-run", "-n", help="Validate seed without executing."),
@@ -156,6 +258,24 @@ def workflow(
156
258
 
157
259
  Use --orchestrator to execute via Claude Agent SDK (Epic 8).
158
260
  Use --resume with --orchestrator to continue a previous session.
261
+ Use --mcp-config to connect to external MCP servers for additional tools.
262
+
263
+ MCP Configuration File Format (YAML):
264
+
265
+ mcp_servers:
266
+ - name: "filesystem"
267
+ transport: "stdio"
268
+ command: "npx"
269
+ args: ["-y", "@anthropic/mcp-server-filesystem", "/workspace"]
270
+ - name: "github"
271
+ transport: "stdio"
272
+ command: "npx"
273
+ args: ["-y", "@anthropic/mcp-server-github"]
274
+ env:
275
+ GITHUB_TOKEN: "${GITHUB_TOKEN}"
276
+ connection:
277
+ timeout_seconds: 30
278
+ retry_attempts: 3
159
279
 
160
280
  Examples:
161
281
 
@@ -165,9 +285,22 @@ def workflow(
165
285
  # Orchestrator mode (Claude Agent SDK)
166
286
  ouroboros run workflow --orchestrator seed.yaml
167
287
 
288
+ # With MCP server integration
289
+ ouroboros run workflow --orchestrator --mcp-config mcp.yaml seed.yaml
290
+
291
+ # With MCP tool prefix
292
+ ouroboros run workflow -o --mcp-config mcp.yaml --mcp-tool-prefix "ext_" seed.yaml
293
+
168
294
  # Resume a previous orchestrator session
169
295
  ouroboros run workflow --orchestrator --resume orch_abc123 seed.yaml
170
296
  """
297
+ # Validate MCP config requires orchestrator mode
298
+ if mcp_config and not orchestrator and not resume_session:
299
+ print_warning(
300
+ "--mcp-config requires --orchestrator flag. Enabling orchestrator mode."
301
+ )
302
+ orchestrator = True
303
+
171
304
  if orchestrator or resume_session:
172
305
  # Orchestrator mode
173
306
  if resume_session and not orchestrator:
@@ -175,7 +308,12 @@ def workflow(
175
308
  "[yellow]Warning: --resume requires --orchestrator flag. "
176
309
  "Enabling orchestrator mode.[/yellow]"
177
310
  )
178
- asyncio.run(_run_orchestrator(seed_file, resume_session))
311
+ asyncio.run(_run_orchestrator(
312
+ seed_file,
313
+ resume_session,
314
+ mcp_config,
315
+ mcp_tool_prefix,
316
+ ))
179
317
  else:
180
318
  # Standard workflow (placeholder)
181
319
  print_info(f"Would execute workflow from: {seed_file}")
ouroboros/cli/main.py CHANGED
@@ -9,7 +9,7 @@ from typing import Annotated
9
9
  import typer
10
10
 
11
11
  from ouroboros import __version__
12
- from ouroboros.cli.commands import config, init, run, status
12
+ from ouroboros.cli.commands import config, init, mcp, run, status
13
13
  from ouroboros.cli.formatters import console
14
14
 
15
15
  # Create the main Typer app
@@ -25,6 +25,7 @@ app.add_typer(init.app, name="init")
25
25
  app.add_typer(run.app, name="run")
26
26
  app.add_typer(config.app, name="config")
27
27
  app.add_typer(status.app, name="status")
28
+ app.add_typer(mcp.app, name="mcp")
28
29
 
29
30
 
30
31
  def version_callback(value: bool) -> None: