shell-mcp: don't block the event loop; honor longer timeouts
subprocess.run inside the async handler serialized every request behind the slowest command (found 2026-08-01 when a du wedged the server mid- migration). Run it in the default thread pool via run_in_executor so concurrent calls actually run concurrently. Timeout default 30->60, cap 300->600, schema text matched.
This commit is contained in:
+16
-5
@@ -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())
|
||||
|
||||
Reference in New Issue
Block a user