Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 61cbf41900 | |||
| 81ff649ab7 |
@@ -0,0 +1,6 @@
|
|||||||
|
"""Allow running as: python -m mcp_synology_container."""
|
||||||
|
|
||||||
|
from mcp_synology_container.cli import main
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -284,34 +284,46 @@ async def _run_check(config_path: str | None) -> bool:
|
|||||||
@click.option("--config", "-c", "config_path", type=click.Path(), help="Config file path")
|
@click.option("--config", "-c", "config_path", type=click.Path(), help="Config file path")
|
||||||
def serve(config_path: str | None) -> None:
|
def serve(config_path: str | None) -> None:
|
||||||
"""Start the MCP server in stdio mode."""
|
"""Start the MCP server in stdio mode."""
|
||||||
|
sys.stderr.write("SERVER STARTING\n")
|
||||||
|
sys.stderr.flush()
|
||||||
_configure_logging("warning")
|
_configure_logging("warning")
|
||||||
asyncio.run(_run_serve(config_path))
|
# Use anyio.run() — FastMCP uses anyio internally (anyio.create_task_group),
|
||||||
|
# and anyio.run() ensures the correct backend context is set up.
|
||||||
|
# asyncio.run() can cause issues on Windows (ProactorEventLoop + anyio).
|
||||||
|
import anyio
|
||||||
|
anyio.run(_run_serve, config_path)
|
||||||
|
|
||||||
|
|
||||||
async def _run_serve(config_path: str | None) -> None:
|
async def _run_serve(config_path: str | None) -> None:
|
||||||
"""Initialize and run the MCP server."""
|
"""Initialize and run the MCP server."""
|
||||||
|
# Eagerly import all modules so any ImportError surfaces immediately on stderr
|
||||||
|
# instead of silently killing the process.
|
||||||
from mcp_synology_container.auth import AuthManager
|
from mcp_synology_container.auth import AuthManager
|
||||||
from mcp_synology_container.config import load_config
|
from mcp_synology_container.config import load_config
|
||||||
from mcp_synology_container.dsm_client import DsmClient
|
from mcp_synology_container.dsm_client import DsmClient
|
||||||
from mcp_synology_container.server import create_server
|
from mcp_synology_container.server import create_server
|
||||||
|
|
||||||
|
logger.debug("Loading config from: %s", config_path or "default path")
|
||||||
try:
|
try:
|
||||||
config = load_config(config_path)
|
config = load_config(config_path)
|
||||||
except (FileNotFoundError, ValueError) as e:
|
except (FileNotFoundError, ValueError) as e:
|
||||||
click.echo(click.style(f"Config error: {e}", fg="red"), err=True)
|
sys.stderr.write(f"Config error: {e}\n")
|
||||||
sys.exit(1)
|
sys.stderr.flush()
|
||||||
|
return
|
||||||
|
|
||||||
|
# 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:
|
async with DsmClient(config.base_url, config.connection.verify_ssl) as client:
|
||||||
await client.query_api_info()
|
|
||||||
auth = AuthManager(config)
|
auth = AuthManager(config)
|
||||||
client.set_auth_manager(auth)
|
client.set_auth_manager(auth)
|
||||||
# Login on startup
|
|
||||||
try:
|
|
||||||
client.sid = await auth.login(client)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error("Initial login failed: %s", e)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
mcp_server = create_server(config, client)
|
mcp_server = create_server(config, client)
|
||||||
logger.info("MCP server starting (stdio mode)")
|
sys.stderr.write("MCP server ready\n")
|
||||||
await mcp_server.run_stdio_async()
|
sys.stderr.flush()
|
||||||
|
try:
|
||||||
|
await mcp_server.run_stdio_async()
|
||||||
|
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._sid: str | None = None
|
||||||
self._auth_manager: AuthManager | None = None
|
self._auth_manager: AuthManager | None = None
|
||||||
self._reauth_lock = asyncio.Lock()
|
self._reauth_lock = asyncio.Lock()
|
||||||
|
self._init_lock = asyncio.Lock()
|
||||||
|
self._initialized = False
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"DsmClient: base_url=%s verify_ssl=%s timeout=%d",
|
"DsmClient: base_url=%s verify_ssl=%s timeout=%d",
|
||||||
self._base_url,
|
self._base_url,
|
||||||
@@ -118,6 +120,24 @@ class DsmClient:
|
|||||||
"""Register the AuthManager for automatic re-login on session errors."""
|
"""Register the AuthManager for automatic re-login on session errors."""
|
||||||
self._auth_manager = auth_manager
|
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:
|
async def __aenter__(self) -> DsmClient:
|
||||||
logging.getLogger("httpx").setLevel(logging.WARNING)
|
logging.getLogger("httpx").setLevel(logging.WARNING)
|
||||||
logging.getLogger("httpcore").setLevel(logging.WARNING)
|
logging.getLogger("httpcore").setLevel(logging.WARNING)
|
||||||
@@ -204,6 +224,7 @@ class DsmClient:
|
|||||||
Raises:
|
Raises:
|
||||||
SynologyError: On API errors.
|
SynologyError: On API errors.
|
||||||
"""
|
"""
|
||||||
|
await self._ensure_initialized()
|
||||||
http = self._get_http()
|
http = self._get_http()
|
||||||
|
|
||||||
if api not in self._api_cache:
|
if api not in self._api_cache:
|
||||||
@@ -278,6 +299,7 @@ class DsmClient:
|
|||||||
Response data dict.
|
Response data dict.
|
||||||
"""
|
"""
|
||||||
api = "SYNO.FileStation.Upload"
|
api = "SYNO.FileStation.Upload"
|
||||||
|
await self._ensure_initialized()
|
||||||
http = self._get_http()
|
http = self._get_http()
|
||||||
|
|
||||||
if api not in self._api_cache:
|
if api not in self._api_cache:
|
||||||
@@ -328,6 +350,7 @@ class DsmClient:
|
|||||||
File content as string.
|
File content as string.
|
||||||
"""
|
"""
|
||||||
api = "SYNO.FileStation.Download"
|
api = "SYNO.FileStation.Download"
|
||||||
|
await self._ensure_initialized()
|
||||||
http = self._get_http()
|
http = self._get_http()
|
||||||
|
|
||||||
if api not in self._api_cache:
|
if api not in self._api_cache:
|
||||||
|
|||||||
Reference in New Issue
Block a user