ouroboros-ai 0.2.3__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 (44) hide show
  1. ouroboros/__init__.py +1 -1
  2. ouroboros/bigbang/__init__.py +9 -0
  3. ouroboros/bigbang/interview.py +16 -18
  4. ouroboros/bigbang/ontology.py +180 -0
  5. ouroboros/cli/commands/__init__.py +2 -0
  6. ouroboros/cli/commands/init.py +162 -97
  7. ouroboros/cli/commands/mcp.py +161 -0
  8. ouroboros/cli/commands/run.py +165 -27
  9. ouroboros/cli/main.py +2 -1
  10. ouroboros/core/ontology_aspect.py +455 -0
  11. ouroboros/core/ontology_questions.py +462 -0
  12. ouroboros/evaluation/__init__.py +16 -1
  13. ouroboros/evaluation/consensus.py +569 -11
  14. ouroboros/evaluation/models.py +81 -0
  15. ouroboros/events/ontology.py +135 -0
  16. ouroboros/mcp/__init__.py +83 -0
  17. ouroboros/mcp/client/__init__.py +20 -0
  18. ouroboros/mcp/client/adapter.py +632 -0
  19. ouroboros/mcp/client/manager.py +600 -0
  20. ouroboros/mcp/client/protocol.py +161 -0
  21. ouroboros/mcp/errors.py +377 -0
  22. ouroboros/mcp/resources/__init__.py +22 -0
  23. ouroboros/mcp/resources/handlers.py +328 -0
  24. ouroboros/mcp/server/__init__.py +21 -0
  25. ouroboros/mcp/server/adapter.py +408 -0
  26. ouroboros/mcp/server/protocol.py +291 -0
  27. ouroboros/mcp/server/security.py +636 -0
  28. ouroboros/mcp/tools/__init__.py +24 -0
  29. ouroboros/mcp/tools/definitions.py +351 -0
  30. ouroboros/mcp/tools/registry.py +269 -0
  31. ouroboros/mcp/types.py +333 -0
  32. ouroboros/orchestrator/__init__.py +31 -0
  33. ouroboros/orchestrator/events.py +40 -0
  34. ouroboros/orchestrator/mcp_config.py +419 -0
  35. ouroboros/orchestrator/mcp_tools.py +483 -0
  36. ouroboros/orchestrator/runner.py +119 -2
  37. ouroboros/providers/claude_code_adapter.py +75 -0
  38. ouroboros/strategies/__init__.py +23 -0
  39. ouroboros/strategies/devil_advocate.py +197 -0
  40. {ouroboros_ai-0.2.3.dist-info → ouroboros_ai-0.4.0.dist-info}/METADATA +73 -17
  41. {ouroboros_ai-0.2.3.dist-info → ouroboros_ai-0.4.0.dist-info}/RECORD +44 -19
  42. {ouroboros_ai-0.2.3.dist-info → ouroboros_ai-0.4.0.dist-info}/WHEEL +0 -0
  43. {ouroboros_ai-0.2.3.dist-info → ouroboros_ai-0.4.0.dist-info}/entry_points.txt +0 -0
  44. {ouroboros_ai-0.2.3.dist-info → ouroboros_ai-0.4.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,483 @@
1
+ """MCP Tool Provider for OrchestratorRunner.
2
+
3
+ This module provides the MCPToolProvider class that wraps external MCP tools
4
+ as agent-callable tools during workflow execution.
5
+
6
+ Features:
7
+ - Converts MCPClientManager tools to agent tool format
8
+ - Handles tool execution with configurable timeouts
9
+ - Implements retry policy for transient failures
10
+ - Provides graceful error handling (no crashes on MCP failures)
11
+
12
+ Usage:
13
+ provider = MCPToolProvider(mcp_manager)
14
+ tools = await provider.get_tools()
15
+ result = await provider.call_tool("tool_name", {"arg": "value"})
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import asyncio
21
+ from collections.abc import Sequence
22
+ from dataclasses import dataclass, field
23
+ from typing import TYPE_CHECKING, Any
24
+
25
+ import stamina
26
+
27
+ from ouroboros.core.types import Result
28
+ from ouroboros.mcp.errors import (
29
+ MCPClientError,
30
+ MCPConnectionError,
31
+ MCPTimeoutError,
32
+ MCPToolError,
33
+ )
34
+ from ouroboros.mcp.types import MCPToolDefinition, MCPToolResult
35
+ from ouroboros.observability.logging import get_logger
36
+
37
+ if TYPE_CHECKING:
38
+ from ouroboros.mcp.client.manager import MCPClientManager
39
+
40
+ log = get_logger(__name__)
41
+
42
+
43
+ # Default timeout for tool execution (30 seconds)
44
+ DEFAULT_TOOL_TIMEOUT = 30.0
45
+
46
+ # Maximum retries for transient failures
47
+ MAX_RETRIES = 3
48
+
49
+ # Retry wait range (exponential backoff)
50
+ RETRY_WAIT_MIN = 0.5
51
+ RETRY_WAIT_MAX = 5.0
52
+
53
+
54
+ @dataclass(frozen=True, slots=True)
55
+ class ToolConflict:
56
+ """Information about a tool name conflict.
57
+
58
+ Attributes:
59
+ tool_name: Name of the conflicting tool.
60
+ source: Where the conflict originated (built-in, server name).
61
+ shadowed_by: What is shadowing this tool.
62
+ resolution: How the conflict was resolved.
63
+ """
64
+
65
+ tool_name: str
66
+ source: str
67
+ shadowed_by: str
68
+ resolution: str
69
+
70
+
71
+ @dataclass(frozen=True, slots=True)
72
+ class MCPToolInfo:
73
+ """Information about an available MCP tool.
74
+
75
+ Attributes:
76
+ name: Tool name (possibly prefixed).
77
+ original_name: Original tool name from MCP server.
78
+ server_name: Name of the MCP server providing this tool.
79
+ description: Tool description.
80
+ input_schema: JSON Schema for tool parameters.
81
+ """
82
+
83
+ name: str
84
+ original_name: str
85
+ server_name: str
86
+ description: str
87
+ input_schema: dict[str, Any] = field(default_factory=dict)
88
+
89
+
90
+ class MCPToolProvider:
91
+ """Provider for MCP tools to integrate with OrchestratorRunner.
92
+
93
+ This class wraps an MCPClientManager and provides:
94
+ - Tool discovery and conversion to agent format
95
+ - Tool execution with timeout handling
96
+ - Retry policy for transient failures
97
+ - Graceful error handling
98
+
99
+ All errors are wrapped and returned as results, not raised as exceptions,
100
+ to ensure MCP failures don't crash the orchestrator.
101
+
102
+ Example:
103
+ manager = MCPClientManager()
104
+ await manager.add_server(config)
105
+ await manager.connect_all()
106
+
107
+ provider = MCPToolProvider(manager)
108
+ tools = await provider.get_tools()
109
+
110
+ result = await provider.call_tool("file_read", {"path": "/tmp/test"})
111
+ if result.is_ok:
112
+ print(result.value.text_content)
113
+ """
114
+
115
+ def __init__(
116
+ self,
117
+ mcp_manager: MCPClientManager,
118
+ *,
119
+ default_timeout: float = DEFAULT_TOOL_TIMEOUT,
120
+ tool_prefix: str = "",
121
+ ) -> None:
122
+ """Initialize the MCP tool provider.
123
+
124
+ Args:
125
+ mcp_manager: MCPClientManager with connected servers.
126
+ default_timeout: Default timeout for tool execution in seconds.
127
+ tool_prefix: Optional prefix to add to all MCP tool names
128
+ (e.g., "mcp_" to namespace tools).
129
+ """
130
+ self._manager = mcp_manager
131
+ self._default_timeout = default_timeout
132
+ self._tool_prefix = tool_prefix
133
+ self._tool_map: dict[str, MCPToolInfo] = {}
134
+ self._conflicts: list[ToolConflict] = []
135
+
136
+ @property
137
+ def tool_prefix(self) -> str:
138
+ """Return the tool name prefix."""
139
+ return self._tool_prefix
140
+
141
+ @property
142
+ def conflicts(self) -> Sequence[ToolConflict]:
143
+ """Return any tool conflicts detected during tool loading."""
144
+ return tuple(self._conflicts)
145
+
146
+ async def get_tools(
147
+ self,
148
+ builtin_tools: Sequence[str] | None = None,
149
+ ) -> Sequence[MCPToolInfo]:
150
+ """Get all available MCP tools.
151
+
152
+ Discovers tools from all connected MCP servers and converts them
153
+ to the agent tool format. Handles tool name conflicts by:
154
+ - Skipping tools that conflict with built-in tools
155
+ - Using first server's tool when multiple servers provide same name
156
+
157
+ Args:
158
+ builtin_tools: List of built-in tool names to avoid conflicts with.
159
+
160
+ Returns:
161
+ Sequence of MCPToolInfo for available tools.
162
+ """
163
+ builtin_set = set(builtin_tools or [])
164
+ self._tool_map.clear()
165
+ self._conflicts.clear()
166
+
167
+ try:
168
+ mcp_tools = await self._manager.list_all_tools()
169
+ except Exception as e:
170
+ log.error(
171
+ "orchestrator.mcp_tools.list_failed",
172
+ error=str(e),
173
+ )
174
+ return ()
175
+
176
+ # Track which tools we've seen (for server conflict detection)
177
+ seen_tools: dict[str, str] = {} # tool_name -> first_server_name
178
+
179
+ for tool in mcp_tools:
180
+ prefixed_name = f"{self._tool_prefix}{tool.name}"
181
+
182
+ # Check for built-in tool conflict
183
+ if prefixed_name in builtin_set or tool.name in builtin_set:
184
+ self._conflicts.append(
185
+ ToolConflict(
186
+ tool_name=tool.name,
187
+ source=tool.server_name or "unknown",
188
+ shadowed_by="built-in",
189
+ resolution="MCP tool skipped",
190
+ )
191
+ )
192
+ log.warning(
193
+ "orchestrator.mcp_tools.shadowed_by_builtin",
194
+ tool_name=tool.name,
195
+ server=tool.server_name,
196
+ )
197
+ continue
198
+
199
+ # Check for server conflict (same tool from multiple servers)
200
+ if prefixed_name in seen_tools:
201
+ first_server = seen_tools[prefixed_name]
202
+ self._conflicts.append(
203
+ ToolConflict(
204
+ tool_name=tool.name,
205
+ source=tool.server_name or "unknown",
206
+ shadowed_by=first_server,
207
+ resolution="Later server's tool skipped",
208
+ )
209
+ )
210
+ log.warning(
211
+ "orchestrator.mcp_tools.shadowed_by_server",
212
+ tool_name=tool.name,
213
+ server=tool.server_name,
214
+ shadowed_by=first_server,
215
+ )
216
+ continue
217
+
218
+ # Register the tool
219
+ seen_tools[prefixed_name] = tool.server_name or "unknown"
220
+ tool_info = MCPToolInfo(
221
+ name=prefixed_name,
222
+ original_name=tool.name,
223
+ server_name=tool.server_name or "unknown",
224
+ description=tool.description,
225
+ input_schema=tool.to_input_schema(),
226
+ )
227
+ self._tool_map[prefixed_name] = tool_info
228
+
229
+ log.info(
230
+ "orchestrator.mcp_tools.loaded",
231
+ tool_count=len(self._tool_map),
232
+ conflict_count=len(self._conflicts),
233
+ servers=list(set(t.server_name for t in self._tool_map.values())),
234
+ )
235
+
236
+ return tuple(self._tool_map.values())
237
+
238
+ def get_tool_names(self) -> Sequence[str]:
239
+ """Get list of available tool names.
240
+
241
+ Returns:
242
+ Sequence of tool names (with prefix if configured).
243
+ """
244
+ return tuple(self._tool_map.keys())
245
+
246
+ def has_tool(self, name: str) -> bool:
247
+ """Check if a tool is available.
248
+
249
+ Args:
250
+ name: Tool name to check (with prefix if applicable).
251
+
252
+ Returns:
253
+ True if tool is available.
254
+ """
255
+ return name in self._tool_map
256
+
257
+ def get_tool_info(self, name: str) -> MCPToolInfo | None:
258
+ """Get info for a specific tool.
259
+
260
+ Args:
261
+ name: Tool name (with prefix if applicable).
262
+
263
+ Returns:
264
+ MCPToolInfo or None if not found.
265
+ """
266
+ return self._tool_map.get(name)
267
+
268
+ async def call_tool(
269
+ self,
270
+ name: str,
271
+ arguments: dict[str, Any] | None = None,
272
+ *,
273
+ timeout: float | None = None,
274
+ ) -> Result[MCPToolResult, MCPToolError]:
275
+ """Call an MCP tool with the given arguments.
276
+
277
+ Handles:
278
+ - Timeout with configurable duration
279
+ - Retry for transient failures (network errors, connection issues)
280
+ - Graceful error handling (returns error result, doesn't raise)
281
+
282
+ Args:
283
+ name: Tool name (with prefix if applicable).
284
+ arguments: Tool arguments as a dict.
285
+ timeout: Optional timeout override in seconds.
286
+
287
+ Returns:
288
+ Result containing MCPToolResult on success or MCPToolError on failure.
289
+ """
290
+ tool_info = self._tool_map.get(name)
291
+ if not tool_info:
292
+ return Result.err(
293
+ MCPToolError(
294
+ f"Tool not found: {name}",
295
+ tool_name=name,
296
+ is_retriable=False,
297
+ )
298
+ )
299
+
300
+ effective_timeout = timeout or self._default_timeout
301
+
302
+ log.debug(
303
+ "orchestrator.mcp_tools.call_start",
304
+ tool_name=name,
305
+ server=tool_info.server_name,
306
+ timeout=effective_timeout,
307
+ )
308
+
309
+ try:
310
+ # Use stamina for retries on transient failures
311
+ result = await self._call_with_retry(
312
+ tool_info=tool_info,
313
+ arguments=arguments or {},
314
+ timeout=effective_timeout,
315
+ )
316
+ return result
317
+ except Exception as e:
318
+ # Catch any unexpected errors and wrap them
319
+ log.exception(
320
+ "orchestrator.mcp_tools.unexpected_error",
321
+ tool_name=name,
322
+ error=str(e),
323
+ )
324
+ return Result.err(
325
+ MCPToolError(
326
+ f"Unexpected error calling tool {name}: {e}",
327
+ tool_name=name,
328
+ server_name=tool_info.server_name,
329
+ is_retriable=False,
330
+ details={"exception_type": type(e).__name__},
331
+ )
332
+ )
333
+
334
+ async def _call_with_retry(
335
+ self,
336
+ tool_info: MCPToolInfo,
337
+ arguments: dict[str, Any],
338
+ timeout: float,
339
+ ) -> Result[MCPToolResult, MCPToolError]:
340
+ """Call tool with retry logic for transient failures.
341
+
342
+ Uses stamina for exponential backoff retries on:
343
+ - Connection errors
344
+ - Timeout errors (if marked retriable)
345
+ - Other transient MCPClientErrors
346
+
347
+ Args:
348
+ tool_info: Information about the tool to call.
349
+ arguments: Tool arguments.
350
+ timeout: Timeout in seconds.
351
+
352
+ Returns:
353
+ Result containing MCPToolResult or MCPToolError.
354
+ """
355
+
356
+ @stamina.retry(
357
+ on=(MCPConnectionError, asyncio.TimeoutError),
358
+ attempts=MAX_RETRIES,
359
+ wait_initial=RETRY_WAIT_MIN,
360
+ wait_max=RETRY_WAIT_MAX,
361
+ wait_jitter=0.5,
362
+ )
363
+ async def _do_call() -> Result[MCPToolResult, MCPClientError]:
364
+ # Use call_tool with server name for explicit routing
365
+ return await self._manager.call_tool(
366
+ server_name=tool_info.server_name,
367
+ tool_name=tool_info.original_name,
368
+ arguments=arguments,
369
+ timeout=timeout,
370
+ )
371
+
372
+ try:
373
+ result = await _do_call()
374
+ except asyncio.TimeoutError:
375
+ log.warning(
376
+ "orchestrator.mcp_tools.timeout_after_retries",
377
+ tool_name=tool_info.name,
378
+ timeout=timeout,
379
+ )
380
+ return Result.err(
381
+ MCPToolError(
382
+ f"Tool call timed out after {MAX_RETRIES} retries: {tool_info.name}",
383
+ tool_name=tool_info.name,
384
+ server_name=tool_info.server_name,
385
+ is_retriable=False,
386
+ details={"timeout_seconds": timeout, "retries": MAX_RETRIES},
387
+ )
388
+ )
389
+ except MCPConnectionError as e:
390
+ log.warning(
391
+ "orchestrator.mcp_tools.connection_failed_after_retries",
392
+ tool_name=tool_info.name,
393
+ error=str(e),
394
+ )
395
+ return Result.err(
396
+ MCPToolError(
397
+ f"Connection failed after {MAX_RETRIES} retries: {e}",
398
+ tool_name=tool_info.name,
399
+ server_name=tool_info.server_name,
400
+ is_retriable=False,
401
+ details={"retries": MAX_RETRIES},
402
+ )
403
+ )
404
+
405
+ # Convert MCPClientError to MCPToolError for consistency
406
+ if result.is_err:
407
+ error = result.error
408
+ log.warning(
409
+ "orchestrator.mcp_tools.call_failed",
410
+ tool_name=tool_info.name,
411
+ error=str(error),
412
+ )
413
+ return Result.err(
414
+ MCPToolError(
415
+ f"Tool execution failed: {error}",
416
+ tool_name=tool_info.name,
417
+ server_name=tool_info.server_name,
418
+ is_retriable=error.is_retriable if isinstance(error, MCPClientError) else False,
419
+ details={"original_error": str(error)},
420
+ )
421
+ )
422
+
423
+ log.debug(
424
+ "orchestrator.mcp_tools.call_success",
425
+ tool_name=tool_info.name,
426
+ is_error=result.value.is_error,
427
+ )
428
+
429
+ return Result.ok(result.value)
430
+
431
+
432
+ @dataclass(frozen=True, slots=True)
433
+ class MCPToolsLoadedEvent:
434
+ """Event data when MCP tools are loaded.
435
+
436
+ Attributes:
437
+ tool_count: Number of tools loaded.
438
+ server_names: Names of servers providing tools.
439
+ conflict_count: Number of tool conflicts detected.
440
+ conflicts: Details of any conflicts.
441
+ """
442
+
443
+ tool_count: int
444
+ server_names: tuple[str, ...]
445
+ conflict_count: int
446
+ conflicts: tuple[ToolConflict, ...] = field(default_factory=tuple)
447
+
448
+
449
+ def create_mcp_tools_loaded_event(
450
+ session_id: str,
451
+ provider: MCPToolProvider,
452
+ ) -> dict[str, Any]:
453
+ """Create event data for MCP tools loaded.
454
+
455
+ Args:
456
+ session_id: Current session ID.
457
+ provider: MCPToolProvider with loaded tools.
458
+
459
+ Returns:
460
+ Event data dict for inclusion in BaseEvent.
461
+ """
462
+ tools = list(provider._tool_map.values())
463
+ server_names = tuple(set(t.server_name for t in tools))
464
+
465
+ return {
466
+ "session_id": session_id,
467
+ "tool_count": len(tools),
468
+ "server_names": server_names,
469
+ "conflict_count": len(provider.conflicts),
470
+ "tool_names": [t.name for t in tools],
471
+ }
472
+
473
+
474
+ __all__ = [
475
+ "DEFAULT_TOOL_TIMEOUT",
476
+ "MAX_RETRIES",
477
+ "MCPToolError",
478
+ "MCPToolInfo",
479
+ "MCPToolProvider",
480
+ "MCPToolsLoadedEvent",
481
+ "ToolConflict",
482
+ "create_mcp_tools_loaded_event",
483
+ ]
@@ -34,16 +34,19 @@ from ouroboros.core.types import Result
34
34
  from ouroboros.observability.logging import get_logger
35
35
  from ouroboros.orchestrator.adapter import DEFAULT_TOOLS, AgentMessage, ClaudeAgentAdapter
36
36
  from ouroboros.orchestrator.events import (
37
+ create_mcp_tools_loaded_event,
37
38
  create_progress_event,
38
39
  create_session_completed_event,
39
40
  create_session_failed_event,
40
41
  create_session_started_event,
41
42
  create_tool_called_event,
42
43
  )
44
+ from ouroboros.orchestrator.mcp_tools import MCPToolProvider
43
45
  from ouroboros.orchestrator.session import SessionRepository, SessionStatus
44
46
 
45
47
  if TYPE_CHECKING:
46
48
  from ouroboros.core.seed import Seed
49
+ from ouroboros.mcp.client.manager import MCPClientManager
47
50
  from ouroboros.persistence.event_store import EventStore
48
51
 
49
52
  log = get_logger(__name__)
@@ -168,6 +171,9 @@ class OrchestratorRunner:
168
171
 
169
172
  Converts Seed specifications to agent prompts, executes via adapter,
170
173
  tracks progress through event emission, and displays status via Rich.
174
+
175
+ Optionally integrates with external MCP servers via MCPClientManager
176
+ to provide additional tools to the Claude Agent during execution.
171
177
  """
172
178
 
173
179
  def __init__(
@@ -175,6 +181,8 @@ class OrchestratorRunner:
175
181
  adapter: ClaudeAgentAdapter,
176
182
  event_store: EventStore,
177
183
  console: Console | None = None,
184
+ mcp_manager: MCPClientManager | None = None,
185
+ mcp_tool_prefix: str = "",
178
186
  ) -> None:
179
187
  """Initialize orchestrator runner.
180
188
 
@@ -182,11 +190,108 @@ class OrchestratorRunner:
182
190
  adapter: Claude Agent adapter for task execution.
183
191
  event_store: Event store for persistence.
184
192
  console: Rich console for output. Uses default if not provided.
193
+ mcp_manager: Optional MCP client manager for external tool integration.
194
+ When provided, tools from connected MCP servers will be
195
+ made available to the Claude Agent during execution.
196
+ mcp_tool_prefix: Optional prefix to add to MCP tool names to avoid
197
+ conflicts (e.g., "mcp_" makes "read" become "mcp_read").
185
198
  """
186
199
  self._adapter = adapter
187
200
  self._event_store = event_store
188
201
  self._console = console or Console()
189
202
  self._session_repo = SessionRepository(event_store)
203
+ self._mcp_manager: MCPClientManager | None = mcp_manager
204
+ self._mcp_tool_prefix = mcp_tool_prefix
205
+
206
+ @property
207
+ def mcp_manager(self) -> MCPClientManager | None:
208
+ """Return the MCP client manager if configured.
209
+
210
+ Returns:
211
+ The MCPClientManager instance or None if not configured.
212
+ """
213
+ return self._mcp_manager
214
+
215
+ async def _get_merged_tools(
216
+ self,
217
+ session_id: str,
218
+ tool_prefix: str = "",
219
+ ) -> tuple[list[str], MCPToolProvider | None]:
220
+ """Get merged tool list from DEFAULT_TOOLS and MCP tools.
221
+
222
+ If MCP manager is configured, discovers tools from connected servers
223
+ and merges them with DEFAULT_TOOLS. DEFAULT_TOOLS always take priority.
224
+
225
+ Args:
226
+ session_id: Current session ID for event emission.
227
+ tool_prefix: Optional prefix for MCP tool names.
228
+
229
+ Returns:
230
+ Tuple of (merged tool names list, MCPToolProvider or None).
231
+ """
232
+ # Start with default tools
233
+ merged_tools = list(DEFAULT_TOOLS)
234
+
235
+ if self._mcp_manager is None:
236
+ return merged_tools, None
237
+
238
+ # Create provider and get MCP tools
239
+ provider = MCPToolProvider(
240
+ self._mcp_manager,
241
+ tool_prefix=tool_prefix,
242
+ )
243
+
244
+ try:
245
+ mcp_tools = await provider.get_tools(builtin_tools=DEFAULT_TOOLS)
246
+ except Exception as e:
247
+ log.warning(
248
+ "orchestrator.runner.mcp_tools_load_failed",
249
+ session_id=session_id,
250
+ error=str(e),
251
+ )
252
+ return merged_tools, None
253
+
254
+ if not mcp_tools:
255
+ log.info(
256
+ "orchestrator.runner.no_mcp_tools_available",
257
+ session_id=session_id,
258
+ )
259
+ return merged_tools, provider
260
+
261
+ # Add MCP tool names to merged list
262
+ mcp_tool_names = [t.name for t in mcp_tools]
263
+ merged_tools.extend(mcp_tool_names)
264
+
265
+ # Log conflicts
266
+ for conflict in provider.conflicts:
267
+ log.warning(
268
+ "orchestrator.runner.tool_conflict",
269
+ tool_name=conflict.tool_name,
270
+ source=conflict.source,
271
+ shadowed_by=conflict.shadowed_by,
272
+ resolution=conflict.resolution,
273
+ )
274
+
275
+ # Emit MCP tools loaded event
276
+ server_names = tuple(set(t.server_name for t in mcp_tools))
277
+ mcp_event = create_mcp_tools_loaded_event(
278
+ session_id=session_id,
279
+ tool_count=len(mcp_tools),
280
+ server_names=server_names,
281
+ conflict_count=len(provider.conflicts),
282
+ tool_names=mcp_tool_names,
283
+ )
284
+ await self._event_store.append(mcp_event)
285
+
286
+ log.info(
287
+ "orchestrator.runner.mcp_tools_loaded",
288
+ session_id=session_id,
289
+ mcp_tool_count=len(mcp_tools),
290
+ total_tools=len(merged_tools),
291
+ servers=server_names,
292
+ )
293
+
294
+ return merged_tools, provider
190
295
 
191
296
  async def execute_seed(
192
297
  self,
@@ -245,6 +350,12 @@ class OrchestratorRunner:
245
350
  system_prompt = build_system_prompt(seed)
246
351
  task_prompt = build_task_prompt(seed)
247
352
 
353
+ # Get merged tools (DEFAULT_TOOLS + MCP tools if configured)
354
+ merged_tools, mcp_provider = await self._get_merged_tools(
355
+ session_id=tracker.session_id,
356
+ tool_prefix=self._mcp_tool_prefix,
357
+ )
358
+
248
359
  # Execute with progress display
249
360
  messages_processed = 0
250
361
  final_message = ""
@@ -265,7 +376,7 @@ class OrchestratorRunner:
265
376
 
266
377
  async for message in self._adapter.execute_task(
267
378
  prompt=task_prompt,
268
- tools=DEFAULT_TOOLS,
379
+ tools=merged_tools,
269
380
  system_prompt=system_prompt,
270
381
  ):
271
382
  messages_processed += 1
@@ -460,6 +571,12 @@ Note: This is a resumed session. Please continue from where execution was interr
460
571
  # Get Claude Agent session ID if stored
461
572
  agent_session_id = tracker.progress.get("agent_session_id")
462
573
 
574
+ # Get merged tools (DEFAULT_TOOLS + MCP tools if configured)
575
+ merged_tools, mcp_provider = await self._get_merged_tools(
576
+ session_id=session_id,
577
+ tool_prefix=self._mcp_tool_prefix,
578
+ )
579
+
463
580
  start_time = datetime.now(UTC)
464
581
  messages_processed = tracker.messages_processed
465
582
  final_message = ""
@@ -480,7 +597,7 @@ Note: This is a resumed session. Please continue from where execution was interr
480
597
 
481
598
  async for message in self._adapter.execute_task(
482
599
  prompt=resume_prompt,
483
- tools=DEFAULT_TOOLS,
600
+ tools=merged_tools,
484
601
  system_prompt=system_prompt,
485
602
  resume_session_id=agent_session_id,
486
603
  ):