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,636 @@
1
+ """MCP Server security layer.
2
+
3
+ This module provides security features for the MCP server including:
4
+ - Authentication (API key, token validation)
5
+ - Authorization (tool-level permissions)
6
+ - Input validation
7
+ - Rate limiting
8
+ """
9
+
10
+ import asyncio
11
+ from collections.abc import Awaitable, Callable
12
+ from dataclasses import dataclass, field
13
+ from enum import StrEnum
14
+ import hashlib
15
+ import hmac
16
+ import threading
17
+ import time
18
+ from typing import Any, TypeVar
19
+
20
+ import structlog
21
+
22
+ from ouroboros.core.types import Result
23
+ from ouroboros.mcp.errors import MCPAuthError, MCPServerError
24
+
25
+ log = structlog.get_logger(__name__)
26
+
27
+ T = TypeVar("T")
28
+
29
+
30
+ class AuthMethod(StrEnum):
31
+ """Authentication method type."""
32
+
33
+ NONE = "none"
34
+ API_KEY = "api_key"
35
+ BEARER_TOKEN = "bearer_token"
36
+
37
+
38
+ class Permission(StrEnum):
39
+ """Permission levels for tool access."""
40
+
41
+ READ = "read"
42
+ WRITE = "write"
43
+ EXECUTE = "execute"
44
+ ADMIN = "admin"
45
+
46
+
47
+ @dataclass(frozen=True, slots=True)
48
+ class AuthConfig:
49
+ """Authentication configuration.
50
+
51
+ Attributes:
52
+ method: Authentication method to use.
53
+ api_keys: Valid API keys (for API_KEY method).
54
+ token_secret: Secret for token validation (for BEARER_TOKEN method).
55
+ required: Whether authentication is required.
56
+ """
57
+
58
+ method: AuthMethod = AuthMethod.NONE
59
+ api_keys: frozenset[str] = field(default_factory=frozenset)
60
+ token_secret: str | None = None
61
+ required: bool = False
62
+
63
+
64
+ @dataclass(frozen=True, slots=True)
65
+ class RateLimitConfig:
66
+ """Rate limiting configuration.
67
+
68
+ Attributes:
69
+ enabled: Whether rate limiting is enabled.
70
+ requests_per_minute: Maximum requests per minute per client.
71
+ burst_size: Maximum burst size.
72
+ """
73
+
74
+ enabled: bool = False
75
+ requests_per_minute: int = 60
76
+ burst_size: int = 10
77
+
78
+
79
+ @dataclass(frozen=True, slots=True)
80
+ class ToolPermission:
81
+ """Permission configuration for a tool.
82
+
83
+ Attributes:
84
+ tool_name: Name of the tool.
85
+ required_permissions: Permissions required to call this tool.
86
+ allowed_roles: Roles that can access this tool.
87
+ """
88
+
89
+ tool_name: str
90
+ required_permissions: frozenset[Permission] = field(
91
+ default_factory=lambda: frozenset({Permission.EXECUTE})
92
+ )
93
+ allowed_roles: frozenset[str] = field(default_factory=frozenset)
94
+
95
+
96
+ @dataclass(frozen=True, slots=True)
97
+ class AuthContext:
98
+ """Context for an authenticated request.
99
+
100
+ Attributes:
101
+ authenticated: Whether the request is authenticated.
102
+ client_id: Identifier for the client.
103
+ permissions: Granted permissions.
104
+ roles: Assigned roles.
105
+ metadata: Additional auth metadata.
106
+ """
107
+
108
+ authenticated: bool = False
109
+ client_id: str | None = None
110
+ permissions: frozenset[Permission] = field(default_factory=frozenset)
111
+ roles: frozenset[str] = field(default_factory=frozenset)
112
+ metadata: dict[str, Any] = field(default_factory=dict)
113
+
114
+
115
+ class RateLimiter:
116
+ """Token bucket rate limiter.
117
+
118
+ Implements a token bucket algorithm for rate limiting requests
119
+ per client.
120
+ """
121
+
122
+ def __init__(
123
+ self,
124
+ requests_per_minute: int,
125
+ burst_size: int,
126
+ ) -> None:
127
+ """Initialize rate limiter.
128
+
129
+ Args:
130
+ requests_per_minute: Maximum requests per minute.
131
+ burst_size: Maximum burst size (bucket capacity).
132
+ """
133
+ self._rate = requests_per_minute / 60.0 # Requests per second
134
+ self._burst_size = burst_size
135
+ self._buckets: dict[str, tuple[float, float]] = {} # client_id -> (tokens, last_update)
136
+ self._lock = asyncio.Lock()
137
+ self._sync_lock = threading.Lock()
138
+
139
+ async def check(self, client_id: str) -> bool:
140
+ """Check if a request is allowed.
141
+
142
+ Args:
143
+ client_id: Identifier for the client.
144
+
145
+ Returns:
146
+ True if the request is allowed, False if rate limited.
147
+ """
148
+ async with self._lock:
149
+ now = time.monotonic()
150
+ tokens, last_update = self._buckets.get(client_id, (self._burst_size, now))
151
+
152
+ # Add tokens based on time elapsed
153
+ elapsed = now - last_update
154
+ tokens = min(self._burst_size, tokens + elapsed * self._rate)
155
+
156
+ if tokens >= 1:
157
+ self._buckets[client_id] = (tokens - 1, now)
158
+ return True
159
+ else:
160
+ self._buckets[client_id] = (tokens, now)
161
+ return False
162
+
163
+ def reset(self, client_id: str) -> None:
164
+ """Reset rate limit for a client.
165
+
166
+ Args:
167
+ client_id: Identifier for the client.
168
+ """
169
+ with self._sync_lock:
170
+ if client_id in self._buckets:
171
+ del self._buckets[client_id]
172
+
173
+
174
+ class Authenticator:
175
+ """Handles authentication for MCP requests."""
176
+
177
+ def __init__(self, config: AuthConfig) -> None:
178
+ """Initialize authenticator.
179
+
180
+ Args:
181
+ config: Authentication configuration.
182
+ """
183
+ self._config = config
184
+ # Hash API keys for secure comparison
185
+ self._hashed_keys: frozenset[str] = frozenset(
186
+ self._hash_key(key) for key in config.api_keys
187
+ )
188
+
189
+ @staticmethod
190
+ def _hash_key(key: str) -> str:
191
+ """Hash an API key for secure storage and comparison."""
192
+ return hashlib.sha256(key.encode()).hexdigest()
193
+
194
+ def authenticate(
195
+ self,
196
+ credentials: dict[str, str] | None,
197
+ ) -> Result[AuthContext, MCPAuthError]:
198
+ """Authenticate a request.
199
+
200
+ Args:
201
+ credentials: Credentials provided by the client.
202
+
203
+ Returns:
204
+ Result containing auth context or auth error.
205
+ """
206
+ if self._config.method == AuthMethod.NONE:
207
+ return Result.ok(
208
+ AuthContext(
209
+ authenticated=not self._config.required,
210
+ permissions=frozenset(Permission),
211
+ )
212
+ )
213
+
214
+ if not credentials:
215
+ if self._config.required:
216
+ return Result.err(
217
+ MCPAuthError(
218
+ "Authentication required",
219
+ auth_method=self._config.method.value,
220
+ )
221
+ )
222
+ return Result.ok(AuthContext(authenticated=False))
223
+
224
+ if self._config.method == AuthMethod.API_KEY:
225
+ return self._authenticate_api_key(credentials)
226
+ elif self._config.method == AuthMethod.BEARER_TOKEN:
227
+ return self._authenticate_token(credentials)
228
+
229
+ return Result.err(
230
+ MCPAuthError(
231
+ f"Unknown auth method: {self._config.method}",
232
+ auth_method=self._config.method.value,
233
+ )
234
+ )
235
+
236
+ def _authenticate_api_key(
237
+ self,
238
+ credentials: dict[str, str],
239
+ ) -> Result[AuthContext, MCPAuthError]:
240
+ """Authenticate using API key.
241
+
242
+ Args:
243
+ credentials: Must contain 'api_key'.
244
+
245
+ Returns:
246
+ Result containing auth context or auth error.
247
+ """
248
+ api_key = credentials.get("api_key")
249
+ if not api_key:
250
+ return Result.err(
251
+ MCPAuthError(
252
+ "API key required",
253
+ auth_method=AuthMethod.API_KEY.value,
254
+ )
255
+ )
256
+
257
+ hashed = self._hash_key(api_key)
258
+ if hashed in self._hashed_keys:
259
+ log.info("mcp.auth.api_key_valid")
260
+ return Result.ok(
261
+ AuthContext(
262
+ authenticated=True,
263
+ client_id=hashed[:16], # Use prefix as client ID
264
+ permissions=frozenset(Permission),
265
+ )
266
+ )
267
+
268
+ log.warning("mcp.auth.invalid_api_key")
269
+ return Result.err(
270
+ MCPAuthError(
271
+ "Invalid API key",
272
+ auth_method=AuthMethod.API_KEY.value,
273
+ )
274
+ )
275
+
276
+ def _authenticate_token(
277
+ self,
278
+ credentials: dict[str, str],
279
+ ) -> Result[AuthContext, MCPAuthError]:
280
+ """Authenticate using bearer token.
281
+
282
+ Args:
283
+ credentials: Must contain 'token'.
284
+
285
+ Returns:
286
+ Result containing auth context or auth error.
287
+ """
288
+ token = credentials.get("token")
289
+ if not token:
290
+ return Result.err(
291
+ MCPAuthError(
292
+ "Bearer token required",
293
+ auth_method=AuthMethod.BEARER_TOKEN.value,
294
+ )
295
+ )
296
+
297
+ if not self._config.token_secret:
298
+ return Result.err(
299
+ MCPAuthError(
300
+ "Token validation not configured",
301
+ auth_method=AuthMethod.BEARER_TOKEN.value,
302
+ )
303
+ )
304
+
305
+ # Simple token validation (in production, use JWT or similar)
306
+ # Format: client_id:timestamp:signature
307
+ parts = token.split(":")
308
+ if len(parts) != 3:
309
+ return Result.err(
310
+ MCPAuthError(
311
+ "Invalid token format",
312
+ auth_method=AuthMethod.BEARER_TOKEN.value,
313
+ )
314
+ )
315
+
316
+ client_id, timestamp_str, signature = parts
317
+
318
+ # Verify signature
319
+ expected = hmac.new(
320
+ self._config.token_secret.encode(),
321
+ f"{client_id}:{timestamp_str}".encode(),
322
+ hashlib.sha256,
323
+ ).hexdigest()
324
+
325
+ if not hmac.compare_digest(signature, expected):
326
+ log.warning("mcp.auth.invalid_token_signature")
327
+ return Result.err(
328
+ MCPAuthError(
329
+ "Invalid token signature",
330
+ auth_method=AuthMethod.BEARER_TOKEN.value,
331
+ )
332
+ )
333
+
334
+ # Check timestamp (tokens valid for 1 hour, 60s clock skew tolerance)
335
+ try:
336
+ timestamp = int(timestamp_str)
337
+ now = time.time()
338
+ if timestamp > now + 60:
339
+ return Result.err(
340
+ MCPAuthError(
341
+ "Token timestamp is in the future",
342
+ auth_method=AuthMethod.BEARER_TOKEN.value,
343
+ )
344
+ )
345
+ if now - timestamp > 3600:
346
+ return Result.err(
347
+ MCPAuthError(
348
+ "Token expired",
349
+ auth_method=AuthMethod.BEARER_TOKEN.value,
350
+ )
351
+ )
352
+ except ValueError:
353
+ return Result.err(
354
+ MCPAuthError(
355
+ "Invalid token timestamp",
356
+ auth_method=AuthMethod.BEARER_TOKEN.value,
357
+ )
358
+ )
359
+
360
+ log.info("mcp.auth.token_valid", client_id=client_id)
361
+ return Result.ok(
362
+ AuthContext(
363
+ authenticated=True,
364
+ client_id=client_id,
365
+ permissions=frozenset(Permission),
366
+ )
367
+ )
368
+
369
+
370
+ class Authorizer:
371
+ """Handles authorization for MCP tool calls."""
372
+
373
+ def __init__(self) -> None:
374
+ """Initialize authorizer."""
375
+ self._tool_permissions: dict[str, ToolPermission] = {}
376
+
377
+ def register_tool_permission(self, permission: ToolPermission) -> None:
378
+ """Register permission requirements for a tool.
379
+
380
+ Args:
381
+ permission: Permission configuration for the tool.
382
+ """
383
+ self._tool_permissions[permission.tool_name] = permission
384
+
385
+ def authorize(
386
+ self,
387
+ tool_name: str,
388
+ auth_context: AuthContext,
389
+ ) -> Result[None, MCPAuthError]:
390
+ """Check if a request is authorized to call a tool.
391
+
392
+ Args:
393
+ tool_name: Name of the tool being called.
394
+ auth_context: Authentication context.
395
+
396
+ Returns:
397
+ Result.ok(None) if authorized, Result.err otherwise.
398
+ """
399
+ permission = self._tool_permissions.get(tool_name)
400
+
401
+ # If no specific permission is registered, allow authenticated users
402
+ if permission is None:
403
+ if auth_context.authenticated:
404
+ return Result.ok(None)
405
+ return Result.err(
406
+ MCPAuthError(
407
+ f"Authentication required for tool: {tool_name}",
408
+ required_permission=Permission.EXECUTE.value,
409
+ )
410
+ )
411
+
412
+ # Check if user has required permissions
413
+ if not permission.required_permissions.issubset(auth_context.permissions):
414
+ missing = permission.required_permissions - auth_context.permissions
415
+ return Result.err(
416
+ MCPAuthError(
417
+ f"Missing permissions for tool {tool_name}: {missing}",
418
+ required_permission=", ".join(p.value for p in missing),
419
+ )
420
+ )
421
+
422
+ # Check if user has an allowed role (if roles are specified)
423
+ if permission.allowed_roles and not permission.allowed_roles.intersection(
424
+ auth_context.roles
425
+ ):
426
+ return Result.err(
427
+ MCPAuthError(
428
+ f"Role not authorized for tool: {tool_name}",
429
+ required_permission=f"roles: {permission.allowed_roles}",
430
+ )
431
+ )
432
+
433
+ return Result.ok(None)
434
+
435
+
436
+ class InputValidator:
437
+ """Validates tool input arguments."""
438
+
439
+ def __init__(self) -> None:
440
+ """Initialize validator."""
441
+ self._validators: dict[str, Callable[[dict[str, Any]], Result[None, str]]] = {}
442
+
443
+ def register_validator(
444
+ self,
445
+ tool_name: str,
446
+ validator: Callable[[dict[str, Any]], Result[None, str]],
447
+ ) -> None:
448
+ """Register a custom validator for a tool.
449
+
450
+ Args:
451
+ tool_name: Name of the tool.
452
+ validator: Validation function.
453
+ """
454
+ self._validators[tool_name] = validator
455
+
456
+ def validate(
457
+ self,
458
+ tool_name: str,
459
+ arguments: dict[str, Any],
460
+ _schema: dict[str, Any] | None = None,
461
+ ) -> Result[None, MCPServerError]:
462
+ """Validate tool arguments.
463
+
464
+ Args:
465
+ tool_name: Name of the tool.
466
+ arguments: Arguments to validate.
467
+ _schema: Optional JSON schema for validation (reserved for future use).
468
+
469
+ Returns:
470
+ Result.ok(None) if valid, Result.err otherwise.
471
+ """
472
+ # Check for dangerous patterns in string arguments
473
+ dangerous_patterns = [
474
+ "__import__", "subprocess", "os.popen", "os.system",
475
+ "eval(", "exec(", "compile(", "open(",
476
+ ]
477
+ path_traversal_patterns = ["../", "..\\"]
478
+ shell_metacharacters = [";", "|", "&&", "||"]
479
+
480
+ for key, value in arguments.items():
481
+ if isinstance(value, str):
482
+ for pattern in dangerous_patterns:
483
+ if pattern in value:
484
+ return Result.err(
485
+ MCPServerError(
486
+ f"Potentially dangerous input in {key}",
487
+ details={"pattern": pattern},
488
+ )
489
+ )
490
+ for pattern in path_traversal_patterns:
491
+ if pattern in value:
492
+ return Result.err(
493
+ MCPServerError(
494
+ f"Path traversal detected in {key}",
495
+ details={"pattern": pattern},
496
+ )
497
+ )
498
+ for char in shell_metacharacters:
499
+ if char in value:
500
+ return Result.err(
501
+ MCPServerError(
502
+ f"Shell metacharacter detected in {key}",
503
+ details={"pattern": char},
504
+ )
505
+ )
506
+
507
+ # Run custom validator if registered
508
+ if tool_name in self._validators:
509
+ result = self._validators[tool_name](arguments)
510
+ if result.is_err:
511
+ return Result.err(
512
+ MCPServerError(
513
+ f"Validation failed for {tool_name}: {result.error}",
514
+ )
515
+ )
516
+
517
+ return Result.ok(None)
518
+
519
+
520
+ @dataclass
521
+ class SecurityLayer:
522
+ """Combined security layer for MCP server.
523
+
524
+ Provides authentication, authorization, rate limiting, and input validation
525
+ in a single interface.
526
+ """
527
+
528
+ auth_config: AuthConfig = field(default_factory=AuthConfig)
529
+ rate_limit_config: RateLimitConfig = field(default_factory=RateLimitConfig)
530
+
531
+ def __post_init__(self) -> None:
532
+ """Initialize security components."""
533
+ self._authenticator = Authenticator(self.auth_config)
534
+ self._authorizer = Authorizer()
535
+ self._validator = InputValidator()
536
+ self._rate_limiter: RateLimiter | None = None
537
+
538
+ if self.rate_limit_config.enabled:
539
+ self._rate_limiter = RateLimiter(
540
+ self.rate_limit_config.requests_per_minute,
541
+ self.rate_limit_config.burst_size,
542
+ )
543
+
544
+ def register_tool_permission(self, permission: ToolPermission) -> None:
545
+ """Register permission requirements for a tool."""
546
+ self._authorizer.register_tool_permission(permission)
547
+
548
+ def register_validator(
549
+ self,
550
+ tool_name: str,
551
+ validator: Callable[[dict[str, Any]], Result[None, str]],
552
+ ) -> None:
553
+ """Register a custom validator for a tool."""
554
+ self._validator.register_validator(tool_name, validator)
555
+
556
+ async def check_request(
557
+ self,
558
+ tool_name: str,
559
+ arguments: dict[str, Any],
560
+ credentials: dict[str, str] | None = None,
561
+ ) -> Result[AuthContext, MCPServerError]:
562
+ """Check if a request passes all security checks.
563
+
564
+ Args:
565
+ tool_name: Name of the tool being called.
566
+ arguments: Arguments for the tool.
567
+ credentials: Client credentials.
568
+
569
+ Returns:
570
+ Result containing auth context or security error.
571
+ """
572
+ # 1. Authenticate
573
+ auth_result = self._authenticator.authenticate(credentials)
574
+ if auth_result.is_err:
575
+ return Result.err(auth_result.error)
576
+
577
+ auth_context = auth_result.value
578
+
579
+ # 2. Rate limit (if enabled)
580
+ if (
581
+ self._rate_limiter
582
+ and auth_context.client_id
583
+ and not await self._rate_limiter.check(auth_context.client_id)
584
+ ):
585
+ return Result.err(
586
+ MCPServerError(
587
+ "Rate limit exceeded",
588
+ is_retriable=True,
589
+ details={"retry_after": 60},
590
+ )
591
+ )
592
+
593
+ # 3. Authorize
594
+ authz_result = self._authorizer.authorize(tool_name, auth_context)
595
+ if authz_result.is_err:
596
+ return Result.err(authz_result.error)
597
+
598
+ # 4. Validate input
599
+ valid_result = self._validator.validate(tool_name, arguments)
600
+ if valid_result.is_err:
601
+ return Result.err(valid_result.error)
602
+
603
+ return Result.ok(auth_context)
604
+
605
+
606
+ def create_security_middleware(
607
+ security_layer: SecurityLayer,
608
+ ) -> Callable[
609
+ [str, dict[str, Any], dict[str, str] | None, Callable[..., Awaitable[Result[T, MCPServerError]]]],
610
+ Awaitable[Result[T, MCPServerError]],
611
+ ]:
612
+ """Create a security middleware function.
613
+
614
+ Args:
615
+ security_layer: The security layer to use.
616
+
617
+ Returns:
618
+ A middleware function that wraps tool handlers.
619
+ """
620
+
621
+ async def middleware(
622
+ tool_name: str,
623
+ arguments: dict[str, Any],
624
+ credentials: dict[str, str] | None,
625
+ handler: Callable[..., Awaitable[Result[T, MCPServerError]]],
626
+ ) -> Result[T, MCPServerError]:
627
+ """Security middleware that checks requests before calling handlers."""
628
+ check_result = await security_layer.check_request(
629
+ tool_name, arguments, credentials
630
+ )
631
+ if check_result.is_err:
632
+ return Result.err(check_result.error)
633
+
634
+ return await handler(arguments)
635
+
636
+ return middleware
@@ -0,0 +1,24 @@
1
+ """MCP Tools package.
2
+
3
+ This package provides tool registration and management for the MCP server.
4
+
5
+ Public API:
6
+ ToolRegistry: Registry for managing tool handlers
7
+ Tool definitions for Ouroboros functionality
8
+ """
9
+
10
+ from ouroboros.mcp.tools.definitions import (
11
+ OUROBOROS_TOOLS,
12
+ execute_seed_handler,
13
+ query_events_handler,
14
+ session_status_handler,
15
+ )
16
+ from ouroboros.mcp.tools.registry import ToolRegistry
17
+
18
+ __all__ = [
19
+ "ToolRegistry",
20
+ "OUROBOROS_TOOLS",
21
+ "execute_seed_handler",
22
+ "session_status_handler",
23
+ "query_events_handler",
24
+ ]