NousResearch/hermes-agent · error · LSPProtocolError

LSP server binary not found

Error message

LSP server binary not found: {cmd[0]} ({e})

What it means

LSPProtocolError raised when asyncio.create_subprocess_exec fails with FileNotFoundError while spawning the language server — the configured server command's executable does not exist on PATH (or is not executable). Raised during client startup before any LSP handshake happens.

Solutions

  1. Install the missing server (e.g. npm i -g typescript-language-server typescript, or pip install pyright) and confirm `which <cmd[0]>` succeeds in the same environment.
  2. Use an absolute path to the server binary in the LSP server configuration to remove PATH ambiguity.
  3. If a custom env is passed, ensure it includes the directory containing the binary (typically inherit os.environ and append).

Example fix

# before
client = LSPClient(cmd=["pyright-langserver", "--stdio"], cwd=project)
await client.start()  # LSPProtocolError: binary not found

# after
import shutil
server = shutil.which("pyright-langserver") or "/opt/venvs/tools/bin/pyright-langserver"
client = LSPClient(cmd=[server, "--stdio"], cwd=project)
await client.start()
Defensive patterns

Strategy: validation

Validate before calling

import shutil

def server_binary_available(cmd: list[str]) -> bool:
    exe = cmd[0]
    if "/" in exe or "\\" in exe:
        import os
        return os.path.isfile(exe) and os.access(exe, os.X_OK)
    return shutil.which(exe) is not None

Try / catch

try:
    await client.start()
except LSPProtocolError as e:
    if "binary not found" in str(e):
        install_server_and_retry()  # npm/pip install, then recreate the client
    else:
        raise

Prevention

When it happens

Trigger: Starting an LSP session with cmd[0] like 'pyright-langserver', 'typescript-language-server', or 'gopls' when that binary is not installed or not on the subprocess's PATH (note env is passed explicitly, so a custom env without the tool's location also triggers it).

Common situations: Node-based servers installed locally via npm but npx/node_modules/.bin not on PATH; Python tools installed in a different virtualenv than the one running the agent; server name typo in configuration; Windows where the extension (.cmd/.exe) resolution differs.

Related errors


AI-assisted analysis of NousResearch/hermes-agent@c896c09c42 (2026-08-14). Data as JSON: /api/errors/806fe02fff356bbc. Report an issue: GitHub.

Appendix: source

Thrown at agent/lsp/client.py:326

            # process group / session. Without this, the LSP server inherits
            # the gateway's pgid (= TUI parent PID). When mcp_tool's
            # _kill_orphaned_mcp_children races with LSP spawn and sweeps the
            # gateway's child set, it captures the LSP PID, records the
            # inherited pgid, and killpg() then kills the TUI parent itself.
            # See tui_gateway_crash.log "killpg → SIGTERM received" stacks.
            self._proc = await asyncio.create_subprocess_exec(
                cmd[0],
                *cmd[1:],
                stdin=asyncio.subprocess.PIPE,
                stdout=asyncio.subprocess.PIPE,
                stderr=asyncio.subprocess.PIPE,
                env=env,
                cwd=self._cwd,
                start_new_session=True,
                creationflags=creationflags,
            )
        except FileNotFoundError as e:
            raise LSPProtocolError(
                f"LSP server binary not found: {cmd[0]} ({e})"
            ) from e

        # Drain stderr at debug level — if we don't, the pipe buffer
        # fills and the server hangs.
        self._stderr_task = asyncio.create_task(self._drain_stderr())
        # Start the reader loop.
        self._reader_task = asyncio.create_task(self._reader_loop())

    async def _drain_stderr(self) -> None:
        if self._proc is None or self._proc.stderr is None:
            return
        try:
            while True:
                line = await self._proc.stderr.readline()
                if not line:
                    break
                text = line.decode("utf-8", errors="replace").rstrip()

View on GitHub (pinned to c896c09c42)