mirror of
https://github.com/basicmachines-co/basic-memory
synced 2026-06-21 13:47:35 +00:00
08c9273328
Adds composition root containers for API, MCP, and CLI entrypoints to centralize configuration loading, logging initialization, and runtime mode selection. This reduces coupling between modules and runtime concerns. Changes: - Add api/container.py: API composition root with config + logging - Add mcp/container.py: MCP composition root with config + logging + client factories - Add cli/container.py: CLI composition root with config + logging + client factories - Refactor api/app.py to use APIContainer.create() - Refactor mcp/server.py to use MCPContainer.create() - Refactor mcp/async_client.py to delegate to MCPContainer for runtime mode Containers own: - Reading ConfigManager + environment variables - Selecting runtime mode (cloud/local/test) - Providing factories (httpx clients) - Initializing logging for each entrypoint This is foundational work that enables further refactoring to remove direct ConfigManager access from downstream modules. Co-authored-by: Paul Hernandez <phernandez@users.noreply.github.com> Signed-off-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
142 lines
4.8 KiB
Python
142 lines
4.8 KiB
Python
"""Composition root for MCP entrypoint.
|
|
|
|
This module owns:
|
|
- Reading ConfigManager + environment variables
|
|
- Selecting runtime mode (cloud/local/test)
|
|
- Providing factories (httpx clients, repositories, services)
|
|
- Initializing logging for MCP
|
|
|
|
This centralizes composition concerns and reduces coupling between
|
|
modules and runtime environment decisions.
|
|
"""
|
|
|
|
from contextlib import asynccontextmanager, AbstractAsyncContextManager
|
|
from dataclasses import dataclass
|
|
from typing import AsyncIterator, Callable, Optional
|
|
|
|
from httpx import ASGITransport, AsyncClient, Timeout
|
|
from loguru import logger
|
|
|
|
from basic_memory.config import BasicMemoryConfig, ConfigManager, init_mcp_logging
|
|
|
|
|
|
@dataclass
|
|
class MCPContainer:
|
|
"""Composition root for MCP entrypoint.
|
|
|
|
Responsibilities:
|
|
- Configuration loading and caching
|
|
- Logging initialization
|
|
- Runtime mode determination
|
|
- HTTP client factory provision
|
|
|
|
The container is built once at startup and provides factories
|
|
for creating properly configured dependencies.
|
|
"""
|
|
|
|
config: BasicMemoryConfig
|
|
|
|
@classmethod
|
|
def create(cls) -> "MCPContainer":
|
|
"""Build container with all dependencies.
|
|
|
|
This is the single point where we:
|
|
1. Initialize logging for MCP mode (file only, no stdout)
|
|
2. Load configuration from file + environment
|
|
3. Determine runtime mode (cloud/local/test)
|
|
|
|
Returns:
|
|
Configured MCPContainer ready for use
|
|
"""
|
|
# Initialize logging first (MCP: file only, never stdout)
|
|
init_mcp_logging()
|
|
|
|
# Load configuration (merges file + environment variables)
|
|
config = ConfigManager().config
|
|
|
|
return cls(config=config)
|
|
|
|
@property
|
|
def is_cloud_mode(self) -> bool:
|
|
"""Check if running in cloud mode.
|
|
|
|
Returns:
|
|
True if cloud mode enabled via env var or config file
|
|
"""
|
|
return self.config.cloud_mode_enabled
|
|
|
|
@property
|
|
def is_test_env(self) -> bool:
|
|
"""Check if running in test environment.
|
|
|
|
Returns:
|
|
True if test environment detected
|
|
"""
|
|
return self.config.is_test_env
|
|
|
|
def get_client_factory(
|
|
self, override_factory: Optional[Callable[[], AbstractAsyncContextManager[AsyncClient]]] = None
|
|
) -> Callable[[], AbstractAsyncContextManager[AsyncClient]]:
|
|
"""Get HTTP client factory based on runtime mode.
|
|
|
|
Priority order:
|
|
1. Override factory (for dependency injection in tests/cloud)
|
|
2. Cloud mode: HTTP client with auth
|
|
3. Local mode: ASGI transport for in-process calls
|
|
|
|
Args:
|
|
override_factory: Optional factory override for testing/cloud
|
|
|
|
Returns:
|
|
Async context manager factory that yields configured AsyncClient
|
|
"""
|
|
if override_factory:
|
|
return override_factory
|
|
|
|
# Return a factory that creates clients based on current mode
|
|
@asynccontextmanager
|
|
async def _client_factory() -> AsyncIterator[AsyncClient]:
|
|
"""Factory that creates client based on runtime mode."""
|
|
timeout = Timeout(
|
|
connect=10.0, # 10 seconds for connection
|
|
read=30.0, # 30 seconds for reading response
|
|
write=30.0, # 30 seconds for writing request
|
|
pool=30.0, # 30 seconds for connection pool
|
|
)
|
|
|
|
if self.is_cloud_mode:
|
|
# Cloud mode: inject auth when creating client
|
|
from basic_memory.cli.auth import CLIAuth
|
|
|
|
auth = CLIAuth(
|
|
client_id=self.config.cloud_client_id, authkit_domain=self.config.cloud_domain
|
|
)
|
|
token = await auth.get_valid_token()
|
|
|
|
if not token:
|
|
raise RuntimeError(
|
|
"Cloud mode enabled but not authenticated. "
|
|
"Run 'basic-memory cloud login' first."
|
|
)
|
|
|
|
# Auth header set ONCE at client creation
|
|
proxy_base_url = f"{self.config.cloud_host}/proxy"
|
|
logger.info(f"Creating HTTP client for cloud proxy at: {proxy_base_url}")
|
|
async with AsyncClient(
|
|
base_url=proxy_base_url,
|
|
headers={"Authorization": f"Bearer {token}"},
|
|
timeout=timeout,
|
|
) as client:
|
|
yield client
|
|
else:
|
|
# Local mode: ASGI transport for in-process calls
|
|
from basic_memory.api.app import app as fastapi_app
|
|
|
|
logger.info("Creating ASGI client for local Basic Memory API")
|
|
async with AsyncClient(
|
|
transport=ASGITransport(app=fastapi_app), base_url="http://test", timeout=timeout
|
|
) as client:
|
|
yield client
|
|
|
|
return _client_factory
|