mcp-hangar 0.2.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.
- mcp_hangar/__init__.py +139 -0
- mcp_hangar/application/__init__.py +1 -0
- mcp_hangar/application/commands/__init__.py +67 -0
- mcp_hangar/application/commands/auth_commands.py +118 -0
- mcp_hangar/application/commands/auth_handlers.py +296 -0
- mcp_hangar/application/commands/commands.py +59 -0
- mcp_hangar/application/commands/handlers.py +189 -0
- mcp_hangar/application/discovery/__init__.py +21 -0
- mcp_hangar/application/discovery/discovery_metrics.py +283 -0
- mcp_hangar/application/discovery/discovery_orchestrator.py +497 -0
- mcp_hangar/application/discovery/lifecycle_manager.py +315 -0
- mcp_hangar/application/discovery/security_validator.py +414 -0
- mcp_hangar/application/event_handlers/__init__.py +50 -0
- mcp_hangar/application/event_handlers/alert_handler.py +191 -0
- mcp_hangar/application/event_handlers/audit_handler.py +203 -0
- mcp_hangar/application/event_handlers/knowledge_base_handler.py +120 -0
- mcp_hangar/application/event_handlers/logging_handler.py +69 -0
- mcp_hangar/application/event_handlers/metrics_handler.py +152 -0
- mcp_hangar/application/event_handlers/persistent_audit_store.py +217 -0
- mcp_hangar/application/event_handlers/security_handler.py +604 -0
- mcp_hangar/application/mcp/tooling.py +158 -0
- mcp_hangar/application/ports/__init__.py +9 -0
- mcp_hangar/application/ports/observability.py +237 -0
- mcp_hangar/application/queries/__init__.py +52 -0
- mcp_hangar/application/queries/auth_handlers.py +237 -0
- mcp_hangar/application/queries/auth_queries.py +118 -0
- mcp_hangar/application/queries/handlers.py +227 -0
- mcp_hangar/application/read_models/__init__.py +11 -0
- mcp_hangar/application/read_models/provider_views.py +139 -0
- mcp_hangar/application/sagas/__init__.py +11 -0
- mcp_hangar/application/sagas/group_rebalance_saga.py +137 -0
- mcp_hangar/application/sagas/provider_failover_saga.py +266 -0
- mcp_hangar/application/sagas/provider_recovery_saga.py +172 -0
- mcp_hangar/application/services/__init__.py +9 -0
- mcp_hangar/application/services/provider_service.py +208 -0
- mcp_hangar/application/services/traced_provider_service.py +211 -0
- mcp_hangar/bootstrap/runtime.py +328 -0
- mcp_hangar/context.py +178 -0
- mcp_hangar/domain/__init__.py +117 -0
- mcp_hangar/domain/contracts/__init__.py +57 -0
- mcp_hangar/domain/contracts/authentication.py +225 -0
- mcp_hangar/domain/contracts/authorization.py +229 -0
- mcp_hangar/domain/contracts/event_store.py +178 -0
- mcp_hangar/domain/contracts/metrics_publisher.py +59 -0
- mcp_hangar/domain/contracts/persistence.py +383 -0
- mcp_hangar/domain/contracts/provider_runtime.py +146 -0
- mcp_hangar/domain/discovery/__init__.py +20 -0
- mcp_hangar/domain/discovery/conflict_resolver.py +267 -0
- mcp_hangar/domain/discovery/discovered_provider.py +185 -0
- mcp_hangar/domain/discovery/discovery_service.py +412 -0
- mcp_hangar/domain/discovery/discovery_source.py +192 -0
- mcp_hangar/domain/events.py +433 -0
- mcp_hangar/domain/exceptions.py +525 -0
- mcp_hangar/domain/model/__init__.py +70 -0
- mcp_hangar/domain/model/aggregate.py +58 -0
- mcp_hangar/domain/model/circuit_breaker.py +152 -0
- mcp_hangar/domain/model/event_sourced_api_key.py +413 -0
- mcp_hangar/domain/model/event_sourced_provider.py +423 -0
- mcp_hangar/domain/model/event_sourced_role_assignment.py +268 -0
- mcp_hangar/domain/model/health_tracker.py +183 -0
- mcp_hangar/domain/model/load_balancer.py +185 -0
- mcp_hangar/domain/model/provider.py +810 -0
- mcp_hangar/domain/model/provider_group.py +656 -0
- mcp_hangar/domain/model/tool_catalog.py +105 -0
- mcp_hangar/domain/policies/__init__.py +19 -0
- mcp_hangar/domain/policies/provider_health.py +187 -0
- mcp_hangar/domain/repository.py +249 -0
- mcp_hangar/domain/security/__init__.py +85 -0
- mcp_hangar/domain/security/input_validator.py +710 -0
- mcp_hangar/domain/security/rate_limiter.py +387 -0
- mcp_hangar/domain/security/roles.py +237 -0
- mcp_hangar/domain/security/sanitizer.py +387 -0
- mcp_hangar/domain/security/secrets.py +501 -0
- mcp_hangar/domain/services/__init__.py +20 -0
- mcp_hangar/domain/services/audit_service.py +376 -0
- mcp_hangar/domain/services/image_builder.py +328 -0
- mcp_hangar/domain/services/provider_launcher.py +1046 -0
- mcp_hangar/domain/value_objects.py +1138 -0
- mcp_hangar/errors.py +818 -0
- mcp_hangar/fastmcp_server.py +1105 -0
- mcp_hangar/gc.py +134 -0
- mcp_hangar/infrastructure/__init__.py +79 -0
- mcp_hangar/infrastructure/async_executor.py +133 -0
- mcp_hangar/infrastructure/auth/__init__.py +37 -0
- mcp_hangar/infrastructure/auth/api_key_authenticator.py +388 -0
- mcp_hangar/infrastructure/auth/event_sourced_store.py +567 -0
- mcp_hangar/infrastructure/auth/jwt_authenticator.py +360 -0
- mcp_hangar/infrastructure/auth/middleware.py +340 -0
- mcp_hangar/infrastructure/auth/opa_authorizer.py +243 -0
- mcp_hangar/infrastructure/auth/postgres_store.py +659 -0
- mcp_hangar/infrastructure/auth/projections.py +366 -0
- mcp_hangar/infrastructure/auth/rate_limiter.py +311 -0
- mcp_hangar/infrastructure/auth/rbac_authorizer.py +323 -0
- mcp_hangar/infrastructure/auth/sqlite_store.py +624 -0
- mcp_hangar/infrastructure/command_bus.py +112 -0
- mcp_hangar/infrastructure/discovery/__init__.py +110 -0
- mcp_hangar/infrastructure/discovery/docker_source.py +289 -0
- mcp_hangar/infrastructure/discovery/entrypoint_source.py +249 -0
- mcp_hangar/infrastructure/discovery/filesystem_source.py +383 -0
- mcp_hangar/infrastructure/discovery/kubernetes_source.py +247 -0
- mcp_hangar/infrastructure/event_bus.py +260 -0
- mcp_hangar/infrastructure/event_sourced_repository.py +443 -0
- mcp_hangar/infrastructure/event_store.py +396 -0
- mcp_hangar/infrastructure/knowledge_base/__init__.py +259 -0
- mcp_hangar/infrastructure/knowledge_base/contracts.py +202 -0
- mcp_hangar/infrastructure/knowledge_base/memory.py +177 -0
- mcp_hangar/infrastructure/knowledge_base/postgres.py +545 -0
- mcp_hangar/infrastructure/knowledge_base/sqlite.py +513 -0
- mcp_hangar/infrastructure/metrics_publisher.py +36 -0
- mcp_hangar/infrastructure/observability/__init__.py +10 -0
- mcp_hangar/infrastructure/observability/langfuse_adapter.py +534 -0
- mcp_hangar/infrastructure/persistence/__init__.py +33 -0
- mcp_hangar/infrastructure/persistence/audit_repository.py +371 -0
- mcp_hangar/infrastructure/persistence/config_repository.py +398 -0
- mcp_hangar/infrastructure/persistence/database.py +333 -0
- mcp_hangar/infrastructure/persistence/database_common.py +330 -0
- mcp_hangar/infrastructure/persistence/event_serializer.py +280 -0
- mcp_hangar/infrastructure/persistence/event_upcaster.py +166 -0
- mcp_hangar/infrastructure/persistence/in_memory_event_store.py +150 -0
- mcp_hangar/infrastructure/persistence/recovery_service.py +312 -0
- mcp_hangar/infrastructure/persistence/sqlite_event_store.py +386 -0
- mcp_hangar/infrastructure/persistence/unit_of_work.py +409 -0
- mcp_hangar/infrastructure/persistence/upcasters/README.md +13 -0
- mcp_hangar/infrastructure/persistence/upcasters/__init__.py +7 -0
- mcp_hangar/infrastructure/query_bus.py +153 -0
- mcp_hangar/infrastructure/saga_manager.py +401 -0
- mcp_hangar/logging_config.py +209 -0
- mcp_hangar/metrics.py +1007 -0
- mcp_hangar/models.py +31 -0
- mcp_hangar/observability/__init__.py +54 -0
- mcp_hangar/observability/health.py +487 -0
- mcp_hangar/observability/metrics.py +319 -0
- mcp_hangar/observability/tracing.py +433 -0
- mcp_hangar/progress.py +542 -0
- mcp_hangar/retry.py +613 -0
- mcp_hangar/server/__init__.py +120 -0
- mcp_hangar/server/__main__.py +6 -0
- mcp_hangar/server/auth_bootstrap.py +340 -0
- mcp_hangar/server/auth_cli.py +335 -0
- mcp_hangar/server/auth_config.py +305 -0
- mcp_hangar/server/bootstrap.py +735 -0
- mcp_hangar/server/cli.py +161 -0
- mcp_hangar/server/config.py +224 -0
- mcp_hangar/server/context.py +215 -0
- mcp_hangar/server/http_auth_middleware.py +165 -0
- mcp_hangar/server/lifecycle.py +467 -0
- mcp_hangar/server/state.py +117 -0
- mcp_hangar/server/tools/__init__.py +16 -0
- mcp_hangar/server/tools/discovery.py +186 -0
- mcp_hangar/server/tools/groups.py +75 -0
- mcp_hangar/server/tools/health.py +301 -0
- mcp_hangar/server/tools/provider.py +939 -0
- mcp_hangar/server/tools/registry.py +320 -0
- mcp_hangar/server/validation.py +113 -0
- mcp_hangar/stdio_client.py +229 -0
- mcp_hangar-0.2.0.dist-info/METADATA +347 -0
- mcp_hangar-0.2.0.dist-info/RECORD +160 -0
- mcp_hangar-0.2.0.dist-info/WHEEL +4 -0
- mcp_hangar-0.2.0.dist-info/entry_points.txt +2 -0
- mcp_hangar-0.2.0.dist-info/licenses/LICENSE +21 -0
mcp_hangar/gc.py
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""Background workers for garbage collection and health checks."""
|
|
2
|
+
|
|
3
|
+
import threading
|
|
4
|
+
import time
|
|
5
|
+
from typing import Any, Literal, Optional
|
|
6
|
+
|
|
7
|
+
from .domain.contracts.provider_runtime import normalize_state_to_str, ProviderMapping, ProviderRuntime
|
|
8
|
+
from .infrastructure.event_bus import get_event_bus
|
|
9
|
+
from .logging_config import get_logger
|
|
10
|
+
from .metrics import observe_health_check, record_error, record_gc_cycle, record_provider_stop
|
|
11
|
+
|
|
12
|
+
logger = get_logger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class BackgroundWorker:
|
|
16
|
+
"""Generic background worker for GC and health checks.
|
|
17
|
+
|
|
18
|
+
Expects provider storage that supports `.items()` (dict-like) returning
|
|
19
|
+
`(provider_id, provider)` pairs where `provider` satisfies the `ProviderRuntime`
|
|
20
|
+
contract.
|
|
21
|
+
|
|
22
|
+
Works with:
|
|
23
|
+
- Provider aggregates
|
|
24
|
+
- backward-compatibility wrappers (as long as they implement the contract)
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
providers: ProviderMapping,
|
|
30
|
+
interval_s: int = 10,
|
|
31
|
+
task: Literal["gc", "health_check"] = "gc",
|
|
32
|
+
event_bus: Optional[Any] = None,
|
|
33
|
+
):
|
|
34
|
+
"""
|
|
35
|
+
Initialize background worker.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
providers: Dict-like mapping (provider_id -> ProviderRuntime).
|
|
39
|
+
interval_s: Interval between runs in seconds.
|
|
40
|
+
task: Task type - either "gc" (garbage collection) or "health_check".
|
|
41
|
+
event_bus: Optional event bus for publishing events (uses global if not provided).
|
|
42
|
+
"""
|
|
43
|
+
self.providers: ProviderMapping = providers
|
|
44
|
+
self.interval_s = interval_s
|
|
45
|
+
self.task = task
|
|
46
|
+
self._event_bus = event_bus or get_event_bus()
|
|
47
|
+
self.thread = threading.Thread(target=self._loop, daemon=True, name=f"worker-{task}")
|
|
48
|
+
self.running = False
|
|
49
|
+
|
|
50
|
+
def start(self):
|
|
51
|
+
"""Start the background worker thread."""
|
|
52
|
+
if self.running:
|
|
53
|
+
logger.warning("background_worker_already_running", task=self.task)
|
|
54
|
+
return
|
|
55
|
+
|
|
56
|
+
self.running = True
|
|
57
|
+
self.thread.start()
|
|
58
|
+
logger.info("background_worker_started", task=self.task, interval_s=self.interval_s)
|
|
59
|
+
|
|
60
|
+
def stop(self):
|
|
61
|
+
"""Stop the background worker thread."""
|
|
62
|
+
self.running = False
|
|
63
|
+
logger.info("background_worker_stopped", task=self.task)
|
|
64
|
+
|
|
65
|
+
def _publish_events(self, provider: ProviderRuntime) -> None:
|
|
66
|
+
"""Publish all collected events from a provider.
|
|
67
|
+
|
|
68
|
+
ProviderRuntime is expected to support event collection.
|
|
69
|
+
"""
|
|
70
|
+
for event in provider.collect_events():
|
|
71
|
+
try:
|
|
72
|
+
self._event_bus.publish(event)
|
|
73
|
+
except Exception:
|
|
74
|
+
logger.exception("event_publish_failed")
|
|
75
|
+
|
|
76
|
+
def _loop(self):
|
|
77
|
+
"""Main worker loop."""
|
|
78
|
+
while self.running:
|
|
79
|
+
time.sleep(self.interval_s)
|
|
80
|
+
|
|
81
|
+
start_time = time.perf_counter()
|
|
82
|
+
gc_collected = {"idle": 0, "dead": 0}
|
|
83
|
+
|
|
84
|
+
# Get snapshot of providers to avoid holding mapping lock (if any)
|
|
85
|
+
providers_snapshot = list(self.providers.items())
|
|
86
|
+
|
|
87
|
+
for provider_id, provider in providers_snapshot:
|
|
88
|
+
try:
|
|
89
|
+
if self.task == "gc":
|
|
90
|
+
# Garbage collection - shutdown idle providers
|
|
91
|
+
if provider.maybe_shutdown_idle():
|
|
92
|
+
logger.info("gc_shutdown", provider_id=provider_id)
|
|
93
|
+
gc_collected["idle"] += 1
|
|
94
|
+
record_provider_stop(provider_id, "idle")
|
|
95
|
+
|
|
96
|
+
elif self.task == "health_check":
|
|
97
|
+
# Determine whether provider is cold (not started yet)
|
|
98
|
+
state_str = normalize_state_to_str(provider.state)
|
|
99
|
+
is_cold = state_str == "cold"
|
|
100
|
+
|
|
101
|
+
# Active health check
|
|
102
|
+
hc_start = time.perf_counter()
|
|
103
|
+
is_healthy = provider.health_check()
|
|
104
|
+
hc_duration = time.perf_counter() - hc_start
|
|
105
|
+
|
|
106
|
+
consecutive = int(getattr(provider.health, "consecutive_failures", 0))
|
|
107
|
+
|
|
108
|
+
observe_health_check(
|
|
109
|
+
provider=provider_id,
|
|
110
|
+
duration=hc_duration,
|
|
111
|
+
healthy=is_healthy,
|
|
112
|
+
is_cold=is_cold,
|
|
113
|
+
consecutive_failures=consecutive,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
if not is_healthy and not is_cold:
|
|
117
|
+
logger.warning("health_check_unhealthy", provider_id=provider_id)
|
|
118
|
+
|
|
119
|
+
# Publish any collected events
|
|
120
|
+
self._publish_events(provider)
|
|
121
|
+
|
|
122
|
+
except Exception as e:
|
|
123
|
+
record_error("gc", type(e).__name__)
|
|
124
|
+
logger.exception(
|
|
125
|
+
"background_task_failed",
|
|
126
|
+
provider_id=provider_id,
|
|
127
|
+
task=self.task,
|
|
128
|
+
error=str(e),
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
# Record GC cycle metrics
|
|
132
|
+
if self.task == "gc":
|
|
133
|
+
duration = time.perf_counter() - start_time
|
|
134
|
+
record_gc_cycle(duration, gc_collected)
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Infrastructure layer - Technical implementations.
|
|
2
|
+
|
|
3
|
+
This package provides infrastructure components for:
|
|
4
|
+
- Command Bus: CQRS command dispatching
|
|
5
|
+
- Query Bus: CQRS query dispatching
|
|
6
|
+
- Event Bus: Publish/subscribe for domain events
|
|
7
|
+
- Event Store: Append-only event persistence
|
|
8
|
+
- Event Sourced Repository: Provider persistence via event sourcing
|
|
9
|
+
- Saga Manager: Long-running business process orchestration
|
|
10
|
+
|
|
11
|
+
Note: Command classes (StartProviderCommand, etc.) have been moved to
|
|
12
|
+
application.commands to maintain proper layer separation.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from .command_bus import CommandBus, CommandHandler, get_command_bus, reset_command_bus
|
|
16
|
+
from .event_bus import EventBus, EventHandler, get_event_bus, reset_event_bus
|
|
17
|
+
from .event_sourced_repository import EventSourcedProviderRepository, ProviderConfigStore
|
|
18
|
+
from .event_store import (
|
|
19
|
+
ConcurrencyError,
|
|
20
|
+
EventStore,
|
|
21
|
+
EventStoreSnapshot,
|
|
22
|
+
get_event_store,
|
|
23
|
+
InMemoryEventStore,
|
|
24
|
+
StoredEvent,
|
|
25
|
+
)
|
|
26
|
+
from .query_bus import (
|
|
27
|
+
get_query_bus,
|
|
28
|
+
GetProviderHealthQuery,
|
|
29
|
+
GetProviderQuery,
|
|
30
|
+
GetProviderToolsQuery,
|
|
31
|
+
GetSystemMetricsQuery,
|
|
32
|
+
ListProvidersQuery,
|
|
33
|
+
Query,
|
|
34
|
+
QueryBus,
|
|
35
|
+
QueryHandler,
|
|
36
|
+
reset_query_bus,
|
|
37
|
+
)
|
|
38
|
+
from .saga_manager import get_saga_manager, Saga, SagaContext, SagaManager, SagaState, SagaStep
|
|
39
|
+
|
|
40
|
+
__all__ = [
|
|
41
|
+
# Command Bus
|
|
42
|
+
"CommandBus",
|
|
43
|
+
"CommandHandler",
|
|
44
|
+
"get_command_bus",
|
|
45
|
+
"reset_command_bus",
|
|
46
|
+
# Event Bus
|
|
47
|
+
"EventBus",
|
|
48
|
+
"EventHandler",
|
|
49
|
+
"get_event_bus",
|
|
50
|
+
"reset_event_bus",
|
|
51
|
+
# Query Bus
|
|
52
|
+
"Query",
|
|
53
|
+
"QueryBus",
|
|
54
|
+
"QueryHandler",
|
|
55
|
+
"ListProvidersQuery",
|
|
56
|
+
"GetProviderQuery",
|
|
57
|
+
"GetProviderToolsQuery",
|
|
58
|
+
"GetProviderHealthQuery",
|
|
59
|
+
"GetSystemMetricsQuery",
|
|
60
|
+
"get_query_bus",
|
|
61
|
+
"reset_query_bus",
|
|
62
|
+
# Event Store
|
|
63
|
+
"EventStore",
|
|
64
|
+
"InMemoryEventStore",
|
|
65
|
+
"StoredEvent",
|
|
66
|
+
"EventStoreSnapshot",
|
|
67
|
+
"ConcurrencyError",
|
|
68
|
+
"get_event_store",
|
|
69
|
+
# Event Sourced Repository
|
|
70
|
+
"EventSourcedProviderRepository",
|
|
71
|
+
"ProviderConfigStore",
|
|
72
|
+
# Saga
|
|
73
|
+
"Saga",
|
|
74
|
+
"SagaManager",
|
|
75
|
+
"SagaState",
|
|
76
|
+
"SagaStep",
|
|
77
|
+
"SagaContext",
|
|
78
|
+
"get_saga_manager",
|
|
79
|
+
]
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""Async executor for running async operations from sync context.
|
|
2
|
+
|
|
3
|
+
This module provides a clean way to execute async coroutines from
|
|
4
|
+
synchronous code without blocking, using a background thread pool.
|
|
5
|
+
|
|
6
|
+
This solves the common problem of "Event loop is closed" errors
|
|
7
|
+
when trying to use asyncio from sync handlers.
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
from mcp_hangar.infrastructure.async_executor import async_executor
|
|
11
|
+
|
|
12
|
+
# Fire-and-forget
|
|
13
|
+
async_executor.submit(some_coroutine())
|
|
14
|
+
|
|
15
|
+
# With callback
|
|
16
|
+
async_executor.submit(some_coroutine(), on_error=lambda e: logger.error(e))
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import asyncio
|
|
20
|
+
import atexit
|
|
21
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
22
|
+
from typing import Any, Callable, Coroutine, Optional
|
|
23
|
+
|
|
24
|
+
from ..logging_config import get_logger
|
|
25
|
+
|
|
26
|
+
logger = get_logger(__name__)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class AsyncExecutor:
|
|
30
|
+
"""Executes async coroutines from sync context using a thread pool.
|
|
31
|
+
|
|
32
|
+
This class provides a singleton executor that runs async operations
|
|
33
|
+
in background threads, each with their own event loop. This avoids
|
|
34
|
+
the "Event loop is closed" error that occurs when trying to use
|
|
35
|
+
asyncio from sync code.
|
|
36
|
+
|
|
37
|
+
Attributes:
|
|
38
|
+
_executor: Thread pool for running async operations
|
|
39
|
+
_max_workers: Maximum number of concurrent background operations
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
_instance: Optional["AsyncExecutor"] = None
|
|
43
|
+
|
|
44
|
+
def __init__(self, max_workers: int = 4):
|
|
45
|
+
"""Initialize the async executor.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
max_workers: Maximum number of concurrent background threads
|
|
49
|
+
"""
|
|
50
|
+
self._max_workers = max_workers
|
|
51
|
+
self._executor: Optional[ThreadPoolExecutor] = None
|
|
52
|
+
self._started = False
|
|
53
|
+
|
|
54
|
+
@classmethod
|
|
55
|
+
def get_instance(cls) -> "AsyncExecutor":
|
|
56
|
+
"""Get or create the singleton instance."""
|
|
57
|
+
if cls._instance is None:
|
|
58
|
+
cls._instance = cls()
|
|
59
|
+
return cls._instance
|
|
60
|
+
|
|
61
|
+
def _ensure_started(self) -> None:
|
|
62
|
+
"""Ensure the thread pool is started."""
|
|
63
|
+
if not self._started:
|
|
64
|
+
self._executor = ThreadPoolExecutor(max_workers=self._max_workers, thread_name_prefix="async-executor-")
|
|
65
|
+
self._started = True
|
|
66
|
+
# Register cleanup on exit
|
|
67
|
+
atexit.register(self.shutdown)
|
|
68
|
+
|
|
69
|
+
def submit(
|
|
70
|
+
self,
|
|
71
|
+
coro: Coroutine[Any, Any, Any],
|
|
72
|
+
on_success: Optional[Callable[[Any], None]] = None,
|
|
73
|
+
on_error: Optional[Callable[[Exception], None]] = None,
|
|
74
|
+
) -> None:
|
|
75
|
+
"""Submit an async coroutine for background execution.
|
|
76
|
+
|
|
77
|
+
The coroutine will be executed in a background thread with its
|
|
78
|
+
own event loop. This is fire-and-forget by default.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
coro: The coroutine to execute
|
|
82
|
+
on_success: Optional callback on successful completion
|
|
83
|
+
on_error: Optional callback on error (default: log debug)
|
|
84
|
+
"""
|
|
85
|
+
self._ensure_started()
|
|
86
|
+
|
|
87
|
+
def run_coro():
|
|
88
|
+
try:
|
|
89
|
+
result = asyncio.run(coro)
|
|
90
|
+
if on_success:
|
|
91
|
+
try:
|
|
92
|
+
on_success(result)
|
|
93
|
+
except (TypeError, ValueError, RuntimeError) as e:
|
|
94
|
+
logger.debug("async_executor_callback_error", error=str(e))
|
|
95
|
+
except (asyncio.CancelledError, asyncio.TimeoutError, ValueError, RuntimeError, OSError) as e:
|
|
96
|
+
# Handle expected async/runtime errors
|
|
97
|
+
if on_error:
|
|
98
|
+
try:
|
|
99
|
+
on_error(e)
|
|
100
|
+
except (TypeError, ValueError, RuntimeError) as callback_err:
|
|
101
|
+
logger.debug("async_executor_error_callback_failed", error=str(callback_err))
|
|
102
|
+
else:
|
|
103
|
+
logger.debug("async_executor_error", error_type=type(e).__name__, error=str(e))
|
|
104
|
+
|
|
105
|
+
self._executor.submit(run_coro)
|
|
106
|
+
|
|
107
|
+
def shutdown(self, wait: bool = False) -> None:
|
|
108
|
+
"""Shutdown the executor.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
wait: Whether to wait for pending operations to complete
|
|
112
|
+
"""
|
|
113
|
+
if self._executor:
|
|
114
|
+
self._executor.shutdown(wait=wait)
|
|
115
|
+
self._executor = None
|
|
116
|
+
self._started = False
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
# Singleton instance for easy access
|
|
120
|
+
async_executor = AsyncExecutor.get_instance()
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def submit_async(
|
|
124
|
+
coro: Coroutine[Any, Any, Any],
|
|
125
|
+
on_error: Optional[Callable[[Exception], None]] = None,
|
|
126
|
+
) -> None:
|
|
127
|
+
"""Convenience function to submit an async operation.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
coro: The coroutine to execute
|
|
131
|
+
on_error: Optional error callback
|
|
132
|
+
"""
|
|
133
|
+
async_executor.submit(coro, on_error=on_error)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Authentication infrastructure implementations.
|
|
2
|
+
|
|
3
|
+
This module provides concrete implementations of authentication contracts
|
|
4
|
+
defined in the domain layer.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .api_key_authenticator import ApiKeyAuthenticator, InMemoryApiKeyStore
|
|
8
|
+
from .jwt_authenticator import JWKSTokenValidator, JWTAuthenticator, OIDCConfig
|
|
9
|
+
from .middleware import AuthContext, AuthenticationMiddleware, AuthorizationMiddleware
|
|
10
|
+
from .opa_authorizer import OPAAuthorizer
|
|
11
|
+
from .rate_limiter import AuthRateLimitConfig, AuthRateLimiter
|
|
12
|
+
from .rbac_authorizer import InMemoryRoleStore, RBACAuthorizer
|
|
13
|
+
|
|
14
|
+
# Persistent stores - imported lazily to avoid dependency issues
|
|
15
|
+
# Use: from mcp_hangar.infrastructure.auth.sqlite_store import SQLiteApiKeyStore
|
|
16
|
+
# Use: from mcp_hangar.infrastructure.auth.postgres_store import PostgresApiKeyStore
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
# API Key authentication
|
|
20
|
+
"ApiKeyAuthenticator",
|
|
21
|
+
"InMemoryApiKeyStore",
|
|
22
|
+
# JWT/OIDC authentication
|
|
23
|
+
"JWTAuthenticator",
|
|
24
|
+
"JWKSTokenValidator",
|
|
25
|
+
"OIDCConfig",
|
|
26
|
+
# Middleware
|
|
27
|
+
"AuthenticationMiddleware",
|
|
28
|
+
"AuthorizationMiddleware",
|
|
29
|
+
"AuthContext",
|
|
30
|
+
# Rate limiting
|
|
31
|
+
"AuthRateLimiter",
|
|
32
|
+
"AuthRateLimitConfig",
|
|
33
|
+
# Authorization
|
|
34
|
+
"RBACAuthorizer",
|
|
35
|
+
"InMemoryRoleStore",
|
|
36
|
+
"OPAAuthorizer",
|
|
37
|
+
]
|