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.
- ouroboros/__init__.py +1 -1
- ouroboros/bigbang/__init__.py +9 -0
- ouroboros/bigbang/ontology.py +180 -0
- ouroboros/cli/commands/__init__.py +2 -0
- ouroboros/cli/commands/mcp.py +161 -0
- ouroboros/cli/commands/run.py +165 -27
- ouroboros/cli/main.py +2 -1
- ouroboros/core/ontology_aspect.py +455 -0
- ouroboros/core/ontology_questions.py +462 -0
- ouroboros/evaluation/__init__.py +16 -1
- ouroboros/evaluation/consensus.py +569 -11
- ouroboros/evaluation/models.py +81 -0
- ouroboros/events/ontology.py +135 -0
- ouroboros/mcp/__init__.py +83 -0
- ouroboros/mcp/client/__init__.py +20 -0
- ouroboros/mcp/client/adapter.py +632 -0
- ouroboros/mcp/client/manager.py +600 -0
- ouroboros/mcp/client/protocol.py +161 -0
- ouroboros/mcp/errors.py +377 -0
- ouroboros/mcp/resources/__init__.py +22 -0
- ouroboros/mcp/resources/handlers.py +328 -0
- ouroboros/mcp/server/__init__.py +21 -0
- ouroboros/mcp/server/adapter.py +408 -0
- ouroboros/mcp/server/protocol.py +291 -0
- ouroboros/mcp/server/security.py +636 -0
- ouroboros/mcp/tools/__init__.py +24 -0
- ouroboros/mcp/tools/definitions.py +351 -0
- ouroboros/mcp/tools/registry.py +269 -0
- ouroboros/mcp/types.py +333 -0
- ouroboros/orchestrator/__init__.py +31 -0
- ouroboros/orchestrator/events.py +40 -0
- ouroboros/orchestrator/mcp_config.py +419 -0
- ouroboros/orchestrator/mcp_tools.py +483 -0
- ouroboros/orchestrator/runner.py +119 -2
- ouroboros/providers/claude_code_adapter.py +75 -0
- ouroboros/strategies/__init__.py +23 -0
- ouroboros/strategies/devil_advocate.py +197 -0
- {ouroboros_ai-0.3.0.dist-info → ouroboros_ai-0.4.0.dist-info}/METADATA +10 -5
- {ouroboros_ai-0.3.0.dist-info → ouroboros_ai-0.4.0.dist-info}/RECORD +42 -17
- {ouroboros_ai-0.3.0.dist-info → ouroboros_ai-0.4.0.dist-info}/WHEEL +0 -0
- {ouroboros_ai-0.3.0.dist-info → ouroboros_ai-0.4.0.dist-info}/entry_points.txt +0 -0
- {ouroboros_ai-0.3.0.dist-info → ouroboros_ai-0.4.0.dist-info}/licenses/LICENSE +0 -0
ouroboros/__init__.py
CHANGED
ouroboros/bigbang/__init__.py
CHANGED
|
@@ -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"]
|
ouroboros/cli/commands/run.py
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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("
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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(
|
|
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:
|