Fix Claude Desktop loading: lazy NAS connection on first tool call
Previously the server blocked at startup waiting for query_api_info()
and login() before starting the MCP protocol. Claude Desktop has a short
initialization timeout and dropped the server before the handshake started.
Changes:
- DsmClient: add _ensure_initialized() with asyncio.Lock for thread-safe
lazy init; called automatically at the start of request(), upload_text(),
and download_text() on the first use.
- cli.py serve: remove upfront query_api_info() and auth.login() calls;
the server now starts immediately ("MCP server ready" on stderr) and
connects to the NAS on the first tool invocation.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user