Skip to main content
Mibyan runs full language servers — pyright, gopls, rust-analyzer, typescript-language-server, clangd, and ~20 more — as background subprocesses and feeds their semantic diagnostics into the post-write lint check used by write_file and patch. When the agent edits a file, it sees exactly the errors that edit introduced — not just syntax errors, but type errors, undefined names, missing imports, and project-wide semantic issues the language server detects. This is the same architecture top-tier coding agents use. Mibyan ships it self-contained: no editor host required, no plugins to install, no separate daemon to manage.

When LSP runs

LSP is gated on git workspace detection. When the agent’s working directory (or the file being edited) is inside a git repository, LSP runs against that workspace. When neither is in a git repo, LSP stays dormant — useful for messaging gateways where the cwd is the user’s home directory and there’s no project to diagnose. The check is layered: in-process syntax check first (microseconds), then LSP diagnostics second when syntax is clean. A flaky or missing language server can never break a write — every LSP failure path falls back silently to the syntax-only result. Concretely, on every successful write_file or patch:
  1. Mibyan captures a baseline of current diagnostics for the file.
  2. Performs the write.
  3. Re-queries the language server, filters out diagnostics that were already in the baseline, and surfaces only the new ones.
The agent sees output like:
The lint field carries the syntax-check result (microsecond in-process parse via ast.parse, json.loads, etc.); the lsp_diagnostics field carries the semantic diagnostics from the real language server. Two channels, independent signals — the agent sees a syntax-clean file with semantic problems as lint: ok plus a populated lsp_diagnostics.

Workspace trust

Many language servers run code that the project itself ships: pyright executes the configured Python interpreter, typescript-language-server loads the project’s node_modules/typescript, svelte-language-server loads svelte.config.js, rust-analyzer runs cargo check (build scripts, proc-macros) on every save, and servers such as jdtls, kotlin-language-server, elixir-ls, zls or haskell-language-server evaluate the project’s build files (Gradle, mix.exs, build.zig, Cabal/Stack) when they start. That is fine for your own project, but not for a repository the agent has just cloned. Mibyan therefore treats every workspace as untrusted unless it is:
  • the git worktree of a directory you pointed Mibyan at: where you launched it (cd my-app && mibyan), the worktree mibyan -w created, the project a Desktop or TUI session is opened in, or a gateway’s terminal.cwd, or
  • a directory listed under lsp.trusted_workspaces (or any directory below one).
A cd in the agent’s terminal does not move the session’s workspace. Cron jobs and Kanban workers get no automatic trust, because the agent can choose their workdir or workspace; list the directories they should trust under lsp.trusted_workspaces. A checkout nested inside a trusted worktree has its own .git, so it is not trusted, and neither is a git repository at or above your home directory (a dotfiles repo there would otherwise trust everything below it). Trust covers the whole directory you pointed Mibyan at, including anything later cloned into it, and lasts until Mibyan exits. In an untrusted workspace Mibyan denies by default: only the servers below start, each with settings that keep it on Mibyan-side tools. Every other server is skipped, including rust-analyzer, gopls, jdtls, kotlin-language-server, elixir-ls, zls, clojure-lsp, haskell-language-server, lua-language-server, terraform-ls, prisma, astro, vue-language-server (it loads the vueCompilerOptions.plugins a project’s tsconfig.json names) and any server you declare under lsp.servers. The diagnostics log records skipped: untrusted workspace …; add it to lsp.trusted_workspaces, and mibyan lsp status marks those servers [trusted workspaces only]. On a local backend, the post-write shell linters that would use the checkout’s own toolchain are skipped the same way whenever the terminal’s current directory is untrusted: npx tsc (it runs the repository’s node_modules/.bin/tsc, or installs from the registry its .npmrc names) and rustfmt --check (rustup honours the repository’s rust-toolchain.toml). Sandboxed backends (Docker, SSH, Modal, …) are unchanged. Diagnostics that need the project’s dependencies (for example unresolved-import warnings) may be less precise until you trust the workspace.

Supported languages

For “manual” entries, install the server through whatever toolchain manager makes sense for that language (rustup, ghcup, opam, brew, …). Mibyan auto-detects the binary on PATH or in <mibyan_HOME>/lsp/bin/.

PowerShell

PowerShellEditorServices isn’t a single binary — it’s a PowerShell module bundle launched by a pwsh (PowerShell 7+) or powershell host. Setup:
  1. Install PowerShell so pwsh (or Windows powershell) is on PATH.
  2. Download the latest release zip from PowerShellEditorServices releases and extract it.
  3. Point Mibyan at the extracted bundle — the directory that contains PowerShellEditorServices/Start-EditorServices.ps1. Either:
    • set lsp.servers.powershell.command: ["/path/to/bundle"] in config.yaml, or
    • extract it to <mibyan_HOME>/lsp/PowerShellEditorServices, or
    • export PSES_BUNDLE_PATH=/path/to/bundle.
mibyan lsp status reports installed once pwsh is found; if the bundle is missing you’ll see a one-time warning in the logs with the download link.

Laravel Blade

.blade.php templates go to laravel-lsp (Blade, Eloquent, Facades) while plain .php files stay with intelephense. Install it once with Composer and make sure the binary is on PATH (or pin it with lsp.servers.laravel-lsp.command):
Mibyan launches it as laravel-lsp lsp (stdio). There is no auto-install recipe; mibyan lsp status shows manual-only until the binary is found. A few servers are installed alongside a peer dependency that npm won’t auto-pull. typescript-language-server and @vue/language-server require the typescript SDK importable from the same node_modules tree — Mibyan installs typescript@6 (the last JavaScript-based line; TypeScript 7 is the Go port and ships no tsserver.js) together with the server when you run mibyan lsp install typescript / mibyan lsp install vue-language-server or auto-install fires on first use. Vue is pinned to @vue/language-server@2, started with vue.hybridMode: false so it hosts its own TypeScript service. The 3.x line only works behind a client-hosted tsserver tunnel (the VS Code / Neovim setup) that Mibyan’s generic client does not run, so it never publishes diagnostics. If an earlier Mibyan installed 3.x, the log shows a one-time vue-language-server: ... 3.x warning; delete <mibyan_HOME>/lsp/node_modules/@vue and <mibyan_HOME>/lsp/bin/vue-language-server*, then run mibyan lsp install vue-language-server (the recipe co-installs the TypeScript SDK).

CLI

mibyan lsp status is the best starting point — it shows which languages will get semantic diagnostics today and which need a binary installed.

Configuration

The defaults work for typical setups; nothing to set if the binaries are on PATH.

Per-server keys

  • disabled: true — skip this server entirely even when its extensions match a file.
  • command: [bin, ...args] — pin a custom binary path. Bypasses auto-install.
  • env: {KEY: value} — extra env vars passed to the spawned process. Servers and the npm / go install auto-installers start from Mibyan’ scrubbed child environment (no gateway tokens or provider API keys), so a server that needs one of those gets it only through this key.
  • initialization_options: {...} — merged into the LSP initializationOptions payload sent in the initialize handshake. Server-specific; consult the language server’s docs.

Custom servers

Any lsp.servers key that is not a built-in server id declares your own language server. It needs command and extensions; the other keys are optional. Custom servers are matched before the built-ins, so they can also take over an extension Mibyan already handles.
Custom servers are never auto-installed: put the binary on PATH (or give an absolute path) and mibyan lsp status lists it as installed. A malformed entry is logged and skipped without affecting the other servers.

Installation locations

When install_strategy: auto, Mibyan installs binaries into <mibyan_HOME>/lsp/bin/. NPM packages land in <mibyan_HOME>/lsp/node_modules/ with bin symlinks one level up. Go binaries come from go install with GOBIN pointed at the staging dir. Nothing is ever installed to /usr/local/, ~/.local/, or any other shared location — the staging dir is fully Mibyan-owned and is removed when you reset the profile.

Performance characteristics

LSP servers are lazy-spawned on first use. Editing a Python file in a project that’s never seen .py traffic spawns pyright; the spawn takes 1-3 seconds for most servers (rust-analyzer can take 10+ on a cold project). Subsequent edits in the same workspace re-use the running server. The LSP layer adds a few milliseconds to clean writes when no diagnostics are emitted. When diagnostics are emitted, the wait budget is wait_timeout seconds — typically the server responds in tens of milliseconds for pyright/tsserver and a few seconds for rust-analyzer mid-indexing. Each edit waits twice (a pre-edit baseline snapshot for the delta, then the post-edit re-check), so a server that never answers costs at most 2 × wait_timeout per edit. The very first request against a workspace also pays the spawn and the server’s initial program build; give large projects room with lsp.warmup_timeout (only that first, cold request uses it — the steady-state budget is unchanged) rather than raising wait_timeout, which would let every later edit block for the cold-build duration. A server that fails for a workspace — spawn error, or a request that outran its budget — marks that (server, root) pair broken and every later request for it is skipped (logged once per root at INFO). By default the pair stays broken until mibyan lsp restart or process exit; lsp.broken_retry_seconds: N retries it after N seconds so one transient stall does not cost the workspace its diagnostics for good. A root you never want served — one monorepo whose server cannot finish in any budget — goes in lsp.exclude_roots; other workspaces keep their servers. Diagnostics are freshness-gated: a result only counts when the server produced it for the content of the current edit (a publishDiagnostics push at/after the change, or a pull request answered after it). Slow servers that haven’t re-checked yet result in “no data” for that edit — never in yesterday’s errors being re-reported as current. Servers are kept alive while they’re being used and shut down after lsp.idle_timeout seconds (default 600) with no file activity — a long-running gateway that touches many worktrees no longer accumulates one language-server process per workspace forever. A reaped server is respawned automatically on the next relevant file operation. Set idle_timeout: 0 to disable reaping and hold every server’s index warm for the life of the process. Servers are also released when their workspace goes away, even if they are not idle: removing a Mibyan-managed worktree (mibyan -w session end, Kanban task cleanup, a delegated subagent’s pruned worktree) shuts down that tree’s language servers before git worktree remove runs, and the periodic sweep shuts down any server whose project root no longer exists on disk (deleted outside Mibyan). The sweep is part of the idle reaper, so idle_timeout: 0 also disables deleted-root reaping; the worktree-removal release always runs. A multi-root server only drops the vanished folder and keeps serving its sibling roots. Servers that support multi-root workspaces (currently pyright) run as a single process per Mibyan process: the first Python project spawns it, and every further project root — for example sibling git worktrees edited by parallel subagents — is attached to that same server as an additional workspace folder instead of starting another copy.

Disabling

Set lsp.enabled: false in config.yaml to disable the entire subsystem. The post-write check falls back to the in-process syntax check (ast.parse for Python, json.loads for JSON, etc.) which ships unchanged from earlier versions. To disable a single language without disabling the whole layer:

Troubleshooting

mibyan lsp status shows a server as “missing” The binary isn’t on PATH and isn’t in <mibyan_HOME>/lsp/bin/. Run mibyan lsp install <server_id> to attempt an auto-install, or install the binary manually through the language’s normal toolchain. Backend warnings section in mibyan lsp status Some servers ship as thin wrappers around an external CLI for actual diagnostics — they spawn cleanly and accept requests but never emit errors when the sidecar binary is missing. The most common case is bash-language-server, which delegates diagnostics to shellcheck. When mibyan lsp status shows a Backend warnings section, install the named tool through your OS package manager:
The same warning is logged once at server spawn time in ~/.mibyan/logs/agent.log. Server starts but never returns diagnostics Check ~/.mibyan/logs/agent.log for [agent.lsp.client] entries — both stderr from the language server and protocol errors land there. Some servers (rust-analyzer especially) need to finish a project-wide index before they emit per-file diagnostics; the first edit after server start may complete with no diagnostics, with subsequent edits picking them up. Server crashed A crashed server is added to the broken-set and won’t be retried for the rest of the session. Run mibyan lsp restart to clear the set; the next edit re-spawns. Editing a file outside any git repo By design, LSP only runs inside a git repository. If the project isn’t yet initialized, run git init to enable LSP diagnostics. Otherwise the in-process syntax-only fallback applies.