Fix stdio startup for Claude Desktop compatibility

- Replace asyncio.run() with anyio.run() in serve command: FastMCP uses
  anyio.create_task_group() internally, and anyio.run() ensures the correct
  backend context. asyncio.run() can misbehave on Windows (ProactorEventLoop).
- Add SERVER STARTING / MCP server ready messages to stderr for diagnostics.
- Replace sys.exit(1) with early return + stderr write inside anyio context;
  sys.exit inside anyio.run() is less predictable than a clean return.
- Eagerly import all modules at serve startup so ImportErrors surface on
  stderr immediately instead of silently killing the process.
- Add __main__.py to allow python -m mcp_synology_container invocation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-13 15:02:29 +02:00
parent a0c1b6ed93
commit 81ff649ab7
2 changed files with 45 additions and 17 deletions
+39 -17
View File
@@ -284,34 +284,56 @@ async def _run_check(config_path: str | None) -> bool:
@click.option("--config", "-c", "config_path", type=click.Path(), help="Config file path")
def serve(config_path: str | None) -> None:
"""Start the MCP server in stdio mode."""
sys.stderr.write("SERVER STARTING\n")
sys.stderr.flush()
_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:
"""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.config import load_config
from mcp_synology_container.dsm_client import DsmClient
from mcp_synology_container.dsm_client import DsmClient, SynologyError
from mcp_synology_container.server import create_server
logger.debug("Loading config from: %s", config_path or "default path")
try:
config = load_config(config_path)
except (FileNotFoundError, ValueError) as e:
click.echo(click.style(f"Config error: {e}", fg="red"), err=True)
sys.exit(1)
sys.stderr.write(f"Config error: {e}\n")
sys.stderr.flush()
return
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)
# Login on startup
try:
client.sid = await auth.login(client)
except Exception as e:
logger.error("Initial login failed: %s", e)
sys.exit(1)
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)
mcp_server = create_server(config, client)
logger.info("MCP server starting (stdio mode)")
await mcp_server.run_stdio_async()
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()
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