Simple C++ CMAKE MCP server to give a llama.cpp local AI access to a remote linux shell.
  • C++ 83.6%
  • Shell 8.3%
  • CMake 8.1%
Find a file
kb3gtn 420ba68ccc Echo log lines to stderr by default, add --quiet to suppress it
Previously, setting logging.log_file switched output to the file only
- no console visibility unless you tailed it. Now every log line goes
to stderr in addition to the file (if configured), and --quiet
suppresses just the stderr echo; the log file itself is unaffected
either way.

Verified end-to-end: default run shows the startup/listening lines on
stderr and in the log file; --quiet leaves the log file populated
while stderr stays empty.
2026-07-18 10:58:24 -04:00
cmake Add log rotation and print git version on startup 2026-07-17 23:00:35 -04:00
config Add log rotation and print git version on startup 2026-07-17 23:00:35 -04:00
scripts Add CMake install() rules for make install 2026-07-17 23:30:03 -04:00
src Echo log lines to stderr by default, add --quiet to suppress it 2026-07-18 10:58:24 -04:00
tests Add C++ MCP server exposing read_file, write_file, run_command 2026-07-17 20:36:43 -04:00
.gitignore Install into ~/.local/bin with config in ~/.config/linux-mcp 2026-07-17 21:46:57 -04:00
CMakeLists.txt Add apply_diff tool for patching files with unified diffs 2026-07-18 01:45:32 -04:00
instruction_prompt.txt Add instruction_prompt.txt for AI clients using apply_diff 2026-07-18 01:57:29 -04:00
README.md Echo log lines to stderr by default, add --quiet to suppress it 2026-07-18 10:58:24 -04:00

linux-MCP

A small C++ MCP (Model Context Protocol) server exposing four tools:

  • read_file — read a text file within an allowed directory.
  • write_file — overwrite or append text content to a file within an allowed directory.
  • apply_diff — apply a unified diff (@@ ... @@ hunks, as produced by diff -u or git diff) to an existing file within an allowed directory, instead of resending the whole file through write_file. Context/removed lines must match the file exactly; a mismatched hunk rejects the whole patch and leaves the file untouched. Supports dry_run to validate without writing.
  • run_command — run a command with no shell involved (fork/execvp with an explicit argv array, so shell metacharacters in arguments are never interpreted), subject to a configurable forbidden-command/pattern list.

All four log the file/command they're targeting as an INFO line before acting, unconditionally — not gated by logging.log_tool_calls (which covers full arguments/results for every tool and is off by default since those can contain file contents). Knowing what was touched is worth having on by default:

  • read_file target: <resolved path>
  • write_file start: <resolved path> (mode=overwrite|append, <n> bytes) and, once the write finishes, write_file complete: <resolved path> (<n> bytes) — useful for seeing that a large write is still in progress rather than hung (an ERROR line is logged instead of complete if the write fails partway through).
  • apply_diff start: <resolved path> (<n> hunks[, dry_run]) and, once applied, apply_diff complete: <resolved path> (<n> bytes, +added/-removed lines) (or an ERROR line naming the hunk and line that failed to match, with nothing written).
  • run_command executing: <command> <args...> [cwd=...]

Transport is MCP "Streamable HTTP": a single POST /mcp endpoint speaking JSON-RPC 2.0, so it can be reached over the network (e.g. by a llama.cpp-based agent/orchestrator) rather than only spawned as a child process.

Dependencies

Everything needed (a C++17 compiler, CMake, and the two header-only libraries) is packaged in the standard repos on both Ubuntu/Debian and Arch — no vendored/submoduled code.

Ubuntu / Debian (apt)

./scripts/install_deps.sh

which runs:

sudo apt-get update
sudo apt-get install -y cmake build-essential nlohmann-json3-dev libcpp-httplib-dev
  • cmake — build system
  • build-essentialg++/make and friends
  • nlohmann-json3-dev — JSON library
  • libcpp-httplib-dev — HTTP server library. On Debian/Ubuntu this ships as a compiled libcpp-httplib.so with only a pkg-config file describing it (no CMake package) — CMakeLists.txt accounts for this, see the comment above the find_package(httplib) block.

Arch Linux (pacman)

sudo pacman -S --needed cmake base-devel nlohmann-json cpp-httplib
  • cmake — build system
  • base-develgcc/make and friends (Arch's equivalent of build-essential)
  • nlohmann-json — JSON library
  • cpp-httplib — HTTP server library, packaged header-only on Arch (unlike the Debian/Ubuntu package above, which ships a compiled .so). CMakeLists.txt tries find_package(httplib), then pkg-config, then a manual header/library search, in that order — whichever one matches how the package on your system exposes itself, it should be found.

The apt path above was built and verified end-to-end on this repo's Ubuntu machine. The pacman package names are correct per Arch's package listings, but the build hasn't been run on an Arch machine — if cmake -B build can't find httplib.h, check pacman -Ql cpp-httplib for where it actually installed the header and file an issue.

Install

./scripts/install.sh

is a thin wrapper around the CMake install rules in CMakeLists.txt — equivalent to:

cmake -B build
cmake --build build
cmake --install build   # or: cd build && make install

Either way installs it the way it's meant to run:

  • binary -> ~/.local/bin/mcp_server (CMAKE_INSTALL_PREFIX defaults to ~/.local for this project specifically — no sudo needed — but cmake -B build -DCMAKE_INSTALL_PREFIX=/wherever still overrides it if you want somewhere else)
  • config -> ~/.config/linux-mcp/config.json (respects $XDG_CONFIG_HOME if set)

The config is only written if it doesn't already exist — re-running install (script, make install, or cmake --install) after a git pull rebuilds and reinstalls the binary but never touches an existing config, so your auth_token and any edits survive upgrades. Make sure ~/.local/bin is on your PATH (install.sh warns if it isn't).

If you'd rather just build without installing anything:

cmake -B build
cmake --build build

Configure

Edit ~/.config/linux-mcp/config.json (created by install.sh on first run) before starting the server:

  • auth_tokenmust be changed from CHANGE_ME; the server refuses to start otherwise. Every request must send Authorization: Bearer <auth_token>.
  • server.host / server.port — defaults to 127.0.0.1:8765 (localhost only). Change host to 0.0.0.0 (or a specific interface) to accept connections from other machines — only do this once auth_token is a real secret, since run_command can execute arbitrary (non-forbidden) commands on this machine.
  • filesystem.allowed_roots — directories read_file/write_file are confined to. Defaults to your home directory (install.sh fills this in) — narrow it to a specific project directory if you don't want the tool touching your whole home directory.
  • run_command.forbidden_commands / forbidden_patterns — exact basenames and regexes checked against the command line before spawning.
  • run_command.allowed_working_directories — directories run_command's cwd may be set to. Same home-directory default as above.
  • logging.log_tool_calls — off by default. When true, every tools/call logs the tool name and (truncated) arguments before running, and the tool name, isError, and (truncated) result afterward. Off by default because arguments/results can include file contents or command output you may not want landing in logs.
  • logging.log_file — empty by default (logs go to stderr only, unrotated). install.sh fills this in to ~/.local/state/linux-mcp/mcp_server.log for new installs; set it yourself (or blank it out) to change that. The directory is created automatically if missing. When set, every log line is written to the file and echoed to stderr — pass --quiet on the command line to suppress the stderr echo (the file is unaffected either way).
  • logging.max_size_bytes / logging.max_backups — only apply when log_file is set. The file is rotated once it reaches max_size_bytes (default 10 MiB), keeping up to max_backups old copies as <log_file>.1 .. <log_file>.N (oldest dropped first; default 5). Set max_backups to 0 to rotate without keeping any history.

Generating an auth token

Any random string works, as long as it's unpredictable. Generate one with:

openssl rand -hex 32

or, if openssl isn't available:

python3 -c "import secrets; print(secrets.token_hex(32))"

Paste the output into ~/.config/linux-mcp/config.json as auth_token, and use the same value in every client request's Authorization: Bearer <auth_token> header. Treat it like a password — anyone who has it can run commands on this machine via run_command.

Run

mcp_server

(assuming ~/.local/bin is on your PATH; otherwise ~/.local/bin/mcp_server). With no --config flag, it reads ~/.config/linux-mcp/config.json (or $XDG_CONFIG_HOME/linux-mcp/config.json if that's set). Override with --config <path> or the MCP_CONFIG_PATH environment variable — useful for running a second instance with a different config, e.g. during development against a repo checkout. Pass --quiet to suppress the stderr echo of log lines (useful when logging.log_file is set and you don't want duplicate output on the console; the log file itself is written either way):

./build/mcp_server --config config/config.local.json

Version

On startup, mcp_server logs an INFO line with the git version it was built from (git describe --tags --always --dirty output, e.g. a short commit hash, or a tag if one exists — with a -dirty suffix if the working tree had uncommitted changes at build time). This is regenerated on every cmake --build, not just at configure time, so it always reflects what's actually running.

Verify

./tests/smoke_test.sh

drives the running server with curl: initialize, tools/list, a read_file/ write_file round-trip, an allowed run_command, a forbidden run_command (expected to be rejected), and a request with no Authorization header (expected 401).

Connecting a client

Point an MCP client (Streamable HTTP transport) at http://<host>:<port>/mcp with header Authorization: Bearer <auth_token>.

For LLAMA.CPP MCP server, make sure you are have the llama proxy running.