# efm-langserver: Proxy Existing Linters and Formatters Through LSP

> efm-langserver is a Go-based language server that connects editors to traditional command-line linters and formatters through LSP. It suits teams that already have reliable language tools but lack editor integrations, though configuration depends on external executables and precise output formats.

**mattn/efm-langserver** — General purpose Language Server

- Repository: https://github.com/mattn/efm-langserver
- Stars: 1,556 · Forks: 62
- Language: Go
- License: MIT
- Published: 2026-10-16 · Updated: 2026-10-16 · Language: en
- Canonical page: https://hysenlabs.com/projects/mattn-efm-langserver

## Why efm-langserver exists for command-line tooling

efm-langserver addresses a specific gap between editor features and established developer tools. Linters and formatters commonly exist as standalone programs that read files, accept standard input, print diagnostics, or rewrite content on standard output. Many editors can invoke those programs directly, but the editing experience then lacks the structured LSP model used for diagnostics, formatting requests, hover information, document symbols, and code actions.

The project acts as a proxy rather than implementing language analysis itself. According to the README, it calls configured tools when an editor triggers an LSP operation, translates their output, and returns the result over LSP. This makes it useful for language maintainers or individual developers who have tools such as ESLint, Prettier, hadolint, markdownlint, perlcritic, or language-specific formatters but do not have a native language-server implementation.

The intended user therefore has some configuration responsibility. efm-langserver does not install or bundle formatters and linters. The README names LuaFormatter, yapf, isort, vint, and markdownlint-cli as examples that must be installed separately. The server is better suited to a workstation or development environment where those executables are already available than to a self-contained editor distribution.

It can also serve multiple languages. Configuration examples include tools for CSS, CSV, Dockerfile, ERB, Git commits, HTML, JavaScript, JSON, Lua, Makefiles, Markdown, Elixir, Perl, and PHP. The common mechanism is the same for each tool, but the command syntax and diagnostic format must be adapted to the selected program.

## How efm-langserver routes editor requests to external commands

The architecture has three connected parts: an editor or IDE, the efm-langserver process, and one or more external tools. The editor communicates with efm-langserver over LSP. The server selects a configured tool for the file, invokes it, and converts the command output into LSP responses. The README describes this as bidirectional LSP communication between the editor and the server, followed by communication with the linter or formatter.

Configuration is the central part of that routing. A tool can define a command, arguments, optional stdin behavior, custom output formats, debounce timing, and behavior related to exit codes. For example, the JavaScript ESLint example uses `eslint --stdin --stdin-filename ${INPUT}`, enables `lint-stdin`, and ignores a nonzero exit code. Its `lint-formats` entries include placeholders such as `%l`, `%c`, `%t`, and `%m` for line, column, type, and message information.

The formatter path is separate. The Blade formatter example sets `format-command: 'blade-formatter --stdin'` and `format-stdin: true`. A formatter that edits a file in place can instead be used without stdin mode. This distinction matters because a command may produce replacement text on standard output, modify the current file, or only print diagnostics.

The server supports file selection through `root-markers`. The example configuration uses `.git/`, while the Credo example adds `mix.lock` and `mix.exs`. That allows one editor workspace to use different commands for different projects or language ecosystems. The exact selection behavior is defined by the configured project layout rather than by a built-in language detector.

## Installation and the first LSP configuration

The README documents installation through Go, with Homebrew as an alternative. The Go installation command installs the latest module version into the Go environment. Homebrew provides a package manager route for systems where that is the preferred installation method.

For a standard environment, install the server with:

```bash
go install github.com/mattn/efm-langserver@latest
```

The resulting binary is `efm-langserver`. Its documented command-line flags include `-c` for a `config.yaml` path, `-d` to dump configuration, `-logfile`, `-loglevel`, `-q`, and `-v` for the version. 

For Termux on Android, the README gives a separate installation command because ordinary Linux binaries do not work there:

```bash
GOOS=android go install github.com/mattn/efm-langserver@latest
```

Configuration is read from `config.yaml`, or it can be supplied through a `DidChangeConfiguration` notification. The documented file locations are `$XDG_CONFIG_HOME/efm-langserver/config.yaml` or `$HOME/.config/efm-langserver/config.yaml` on UNIX, and `%APPDATA%\efm-langserver\config.yaml` on Windows. A minimal client-side initialization should advertise the optional capabilities that the editor may use:

```json
{
 "initializationOptions": {
 "documentFormatting": true,
 "documentRangeFormatting": true,
 "hover": true,
 "documentSymbol": true,
 "codeAction": true,
 "completion": true
 }
}
```

The README does not provide a complete editor-independent first-use sequence beyond installation, configuration, and client setup, so the next required step is to add a real command to `config.yaml` and register the server in the chosen editor.

## Configuration files, dynamic settings, and tool ownership

The example `config.yaml` shows the main control surface. Version 2 is used, `root-markers` identifies project roots, `lint-debounce` controls delay before linting, and `commands` defines reusable command entries. Tool-specific sections then connect those commands to language features.

For Markdown, the example defines a lint command, enables stdin mode, and provides two diagnostic patterns:

```yaml
version: 2
root-markers:
  - .git/
lint-debounce: 1s
tools:
  markdown-markdownlint:
    lint-command: 'markdownlint -s -c %USERPROFILE%\.markdownlintrc'
    lint-stdin: true
    lint-formats:
      - '%f:%l %m'
      - '%f:%l:%c %m'
```

This example is specifically written for Windows because it references `%USERPROFILE%`. On another platform, the configuration must use the path syntax supported by the installed tool and operating system. The example also demonstrates why output parsing is not automatic. A formatter can usually return edited text, but a linter needs a format string that identifies the file and diagnostic positions.

Dynamic configuration is available through `DidChangeConfiguration`. The README states that it can be called at any time and overwrites only the properties supplied by the client. That model is useful for switching tool settings without restarting the server. However, the same section documents an explicit boundary: `DidChangeConfiguration` supports only V2 configuration and cannot set `LogFile`. The file-based configuration remains necessary when logging must be configured through `LogFile`.

The repository includes `schema.json` and `schema.md` for the available options. Those files should be treated as the authority for the full configuration surface rather than assuming that the README examples cover every supported key.

## Editor setup requires matching external-tool behavior

The README documents setup for vim-lsp, coc.nvim, Eglot, Neovim's built-in LSP with nvim-lspconfig, Helix, and VSCode. This breadth is useful, but it does not remove the need to align the server with each editor's LSP configuration and with the tools installed on the machine.

The external program must be discoverable by the environment from which efm-langserver starts. A command such as `eslint` or `prettier` will not work merely because it is installed somewhere on the developer's machine if that directory is absent from `PATH`. Commands using project-local paths, such as `./node_modules/.bin/prettier`, depend on the working directory and project layout.

The tool's output contract matters just as much as the command path. For diagnostics, `lint-formats` must match the actual output. The README includes flexible patterns for file names, lines, columns, message types, and messages, but it does not claim that every linter can be integrated without adaptation. Some tools may need flags for stdin, filenames, machine-readable output, or exit-code normalization.

A practical first use is to select one language and one tool, configure its command, and verify that formatting produces the intended file content before enabling several tools. The README's configuration model supports that incremental approach, but it does not document a validation command that checks every tool configuration before use.

## Limitations, alternatives, and maintenance boundaries

The central limitation is architectural. efm-langserver is not a linter or formatter itself, so it cannot provide diagnostics or formatting when the selected executable is absent, incompatible, or incorrectly configured. It also depends on command-line conventions rather than a complete, uniform interface shared by every external tool. A tool that emits inconsistent human-readable output can require a custom `lint-formats` entry.

The project is also not a complete alternative to a language-specific language server. A native server may offer richer semantic features such as completion, symbols, or code actions based on language understanding. efm-langserver can advertise those capabilities through `InitializeParams`, but its README does not claim that it supplies semantic analysis itself. The available results depend on the commands configured for the file.

A real alternative is to use an editor-native language server for the language, or to configure the linter and formatter directly in the editor without an LSP proxy. Native language servers generally provide a language-specific integration rather than translating arbitrary command output. Direct editor commands avoid a proxy layer, but they usually require custom command definitions and may not expose results as LSP diagnostics. efm-langserver is the better fit when one proxy configuration can expose several existing tools through a common LSP surface.

The repository is not archived, and its last push was on 2026-07-08. The most recent listed release is `v0.0.57`, dated 2026-07-08, following `v0.0.56` on 2026-03-08 and `v0.0.55` on 2026-02-01. This gives users recent project activity, but the configuration remains tied to external tools that can change independently. Upgrading efm-langserver may therefore require checking command flags and output formats, not only the server binary.

The repository identifies the license as MIT. The license permits use and modification under its terms, but users should review the actual `LICENSE` file for obligations and compatibility rather than treating this review as legal advice. The Go module declares Go 1.23, and the Makefile provides build, test, schema documentation, release, and upload targets.

## Conclusion

Adopt efm-langserver when an editor needs LSP support for existing command-line linters and formatters and you can maintain their installation and output formats. Do not choose it as a standalone analyzer or formatter package, because it includes none. Verify first that the required tools can run in the editor environment and that their stdout, stderr, stdin, and exit-code behavior matches the configuration.

## FAQ

### How do I install efm-langserver?

The README documents `go install github.com/mattn/efm-langserver@latest` and a Homebrew alternative, `brew install efm-langserver`. On Termux, it specifically requires `GOOS=android go install github.com/mattn/efm-langserver@latest` because ordinary Linux binaries do not work there.

### Does efm-langserver include linters and formatters?

No. The README states that efm-langserver does not include formatters or linters and lists tools such as LuaFormatter, yapf, isort, vint, and markdownlint-cli as programs that must be installed manually.

### Where does efm-langserver look for config.yaml?

On UNIX, the documented paths are `$XDG_CONFIG_HOME/efm-langserver/config.yaml` and `$HOME/.config/efm-langserver/config.yaml`. On Windows, the documented path is `%APPDATA%\efm-langserver\config.yaml`.

### Can efm-langserver configure LogFile through DidChangeConfiguration?

No. The README says that `DidChangeConfiguration` supports V2 configuration but cannot set `LogFile`. It can update other supplied properties at any time and overwrites only those properties.

## Sources

- [Issues](https://github.com/mattn/efm-langserver/issues)
- [License: MIT](https://github.com/mattn/efm-langserver/blob/master/LICENSE)
- [mattn/efm-langserver on GitHub](https://github.com/mattn/efm-langserver)
- [README](https://github.com/mattn/efm-langserver/blob/master/README.md)
- [Releases](https://github.com/mattn/efm-langserver/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/mattn-efm-langserver
