diff --git a/shell-mcp/server.py b/shell-mcp/server.py index 8797d97..cc2ba52 100644 --- a/shell-mcp/server.py +++ b/shell-mcp/server.py @@ -15,9 +15,14 @@ arrsstack Pi 5 (arm64). Differences that matter: * Dependencies are pinned (see requirements.txt). The original installed `mcp starlette uvicorn` unpinned; starlette has since gone 1.x. An unpinned install inside a first-boot script is a time bomb. + * 2026-08-02: commands run in a worker thread (run_in_executor), not + inline in the async handler. The original blocked uvicorn's event loop + for the duration of every command, serializing ALL requests behind the + slowest one — discovered mid-migration when a long du wedged the server. Commands run in the host namespaces via nsenter — full root on the media box. """ +import asyncio import os import shlex import subprocess @@ -69,8 +74,8 @@ async def list_tools() -> list[Tool]: }, "timeout": { "type": "integer", - "description": "Timeout in seconds, default 30, max 300", - "default": 30, + "description": "Timeout in seconds, default 60, max 600", + "default": 60, }, }, "required": ["command"], @@ -86,7 +91,7 @@ async def call_tool(name: str, arguments: dict) -> list[TextContent]: command = arguments["command"] working_dir = arguments.get("working_directory") - timeout = min(int(arguments.get("timeout", 30)), 300) + timeout = min(int(arguments.get("timeout", 60)), 600) inner = f"cd {shlex.quote(working_dir)} && {command}" if working_dir else command @@ -95,8 +100,11 @@ async def call_tool(name: str, arguments: dict) -> list[TextContent]: f"bash -c {shlex.quote(inner)}" ) - try: - result = subprocess.run( + # Run the blocking subprocess in the default thread pool. Doing this inline + # would block uvicorn's event loop and serialize every request behind the + # slowest command — a long du froze the whole server for minutes once. + def _run() -> subprocess.CompletedProcess: + return subprocess.run( host_cmd, shell=True, executable="/bin/bash", @@ -104,6 +112,9 @@ async def call_tool(name: str, arguments: dict) -> list[TextContent]: text=True, timeout=timeout, ) + + try: + result = await asyncio.get_running_loop().run_in_executor(None, _run) parts = [] if result.stdout: parts.append(result.stdout.rstrip())