diff --git a/src/mcp_synology_container/cli.py b/src/mcp_synology_container/cli.py index 3db6b33..9c652eb 100644 --- a/src/mcp_synology_container/cli.py +++ b/src/mcp_synology_container/cli.py @@ -300,7 +300,7 @@ async def _run_serve(config_path: str | None) -> None: # instead of silently killing the process. from mcp_synology_container.auth import AuthManager from mcp_synology_container.config import load_config - from mcp_synology_container.dsm_client import DsmClient, SynologyError + from mcp_synology_container.dsm_client import DsmClient from mcp_synology_container.server import create_server logger.debug("Loading config from: %s", config_path or "default path") @@ -311,29 +311,19 @@ async def _run_serve(config_path: str | None) -> None: sys.stderr.flush() return - logger.debug("Connecting to %s", config.base_url) - try: - async with DsmClient(config.base_url, config.connection.verify_ssl) as client: - await client.query_api_info() - auth = AuthManager(config) - client.set_auth_manager(auth) + # Open the HTTP client and register auth — but do NOT connect to the NAS yet. + # Lazy init (_ensure_initialized) runs on the first tool call, so Claude Desktop + # sees the server as ready immediately without waiting for NAS connectivity. + async with DsmClient(config.base_url, config.connection.verify_ssl) as client: + auth = AuthManager(config) + client.set_auth_manager(auth) - try: - client.sid = await auth.login(client) - except Exception as e: - sys.stderr.write(f"Login failed: {e}\n") - sys.stderr.flush() - return - - logger.debug("Login OK, creating MCP server") - mcp_server = create_server(config, client) - sys.stderr.write("MCP server ready\n") - sys.stderr.flush() + mcp_server = create_server(config, client) + sys.stderr.write("MCP server ready\n") + sys.stderr.flush() + try: await mcp_server.run_stdio_async() - except SynologyError as e: - sys.stderr.write(f"DSM error during startup: {e}\n") - sys.stderr.flush() - except Exception as e: - sys.stderr.write(f"Fatal error: {e}\n") - sys.stderr.flush() - raise + except Exception as e: + sys.stderr.write(f"MCP server error: {e}\n") + sys.stderr.flush() + raise diff --git a/src/mcp_synology_container/dsm_client.py b/src/mcp_synology_container/dsm_client.py index a2933e2..933f130 100644 --- a/src/mcp_synology_container/dsm_client.py +++ b/src/mcp_synology_container/dsm_client.py @@ -98,6 +98,8 @@ class DsmClient: self._sid: str | None = None self._auth_manager: AuthManager | None = None self._reauth_lock = asyncio.Lock() + self._init_lock = asyncio.Lock() + self._initialized = False logger.debug( "DsmClient: base_url=%s verify_ssl=%s timeout=%d", self._base_url, @@ -118,6 +120,24 @@ class DsmClient: """Register the AuthManager for automatic re-login on session errors.""" self._auth_manager = auth_manager + async def _ensure_initialized(self) -> None: + """Connect to NAS and authenticate on first use (lazy init). + + Subsequent calls are no-ops. Thread-safe via asyncio.Lock. + """ + if self._initialized: + return + async with self._init_lock: + if self._initialized: # re-check inside lock + return + logger.debug("Lazy init: querying API info from %s", self._base_url) + await self.query_api_info() + if self._auth_manager: + logger.debug("Lazy init: authenticating") + self._sid = await self._auth_manager.login(self) + self._initialized = True + logger.debug("Lazy init complete") + async def __aenter__(self) -> DsmClient: logging.getLogger("httpx").setLevel(logging.WARNING) logging.getLogger("httpcore").setLevel(logging.WARNING) @@ -204,6 +224,7 @@ class DsmClient: Raises: SynologyError: On API errors. """ + await self._ensure_initialized() http = self._get_http() if api not in self._api_cache: @@ -278,6 +299,7 @@ class DsmClient: Response data dict. """ api = "SYNO.FileStation.Upload" + await self._ensure_initialized() http = self._get_http() if api not in self._api_cache: @@ -328,6 +350,7 @@ class DsmClient: File content as string. """ api = "SYNO.FileStation.Download" + await self._ensure_initialized() http = self._get_http() if api not in self._api_cache: