- C++ 83.6%
- Shell 8.3%
- CMake 8.1%
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. |
||
|---|---|---|
| cmake | ||
| config | ||
| scripts | ||
| src | ||
| tests | ||
| .gitignore | ||
| CMakeLists.txt | ||
| instruction_prompt.txt | ||
| README.md | ||
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 bydiff -uorgit diff) to an existing file within an allowed directory, instead of resending the whole file throughwrite_file. Context/removed lines must match the file exactly; a mismatched hunk rejects the whole patch and leaves the file untouched. Supportsdry_runto validate without writing.run_command— run a command with no shell involved (fork/execvpwith an explicitargvarray, 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 (anERRORline is logged instead ofcompleteif 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 anERRORline 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 systembuild-essential—g++/makeand friendsnlohmann-json3-dev— JSON librarylibcpp-httplib-dev— HTTP server library. On Debian/Ubuntu this ships as a compiledlibcpp-httplib.sowith only a pkg-config file describing it (no CMake package) —CMakeLists.txtaccounts for this, see the comment above thefind_package(httplib)block.
Arch Linux (pacman)
sudo pacman -S --needed cmake base-devel nlohmann-json cpp-httplib
cmake— build systembase-devel—gcc/makeand friends (Arch's equivalent ofbuild-essential)nlohmann-json— JSON librarycpp-httplib— HTTP server library, packaged header-only on Arch (unlike the Debian/Ubuntu package above, which ships a compiled.so).CMakeLists.txttriesfind_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 buildcan't findhttplib.h, checkpacman -Ql cpp-httplibfor 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_PREFIXdefaults to~/.localfor this project specifically — nosudoneeded — butcmake -B build -DCMAKE_INSTALL_PREFIX=/whereverstill overrides it if you want somewhere else) - config ->
~/.config/linux-mcp/config.json(respects$XDG_CONFIG_HOMEif 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_token— must be changed fromCHANGE_ME; the server refuses to start otherwise. Every request must sendAuthorization: Bearer <auth_token>.server.host/server.port— defaults to127.0.0.1:8765(localhost only). Changehostto0.0.0.0(or a specific interface) to accept connections from other machines — only do this onceauth_tokenis a real secret, sincerun_commandcan execute arbitrary (non-forbidden) commands on this machine.filesystem.allowed_roots— directoriesread_file/write_fileare confined to. Defaults to your home directory (install.shfills 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— directoriesrun_command'scwdmay be set to. Same home-directory default as above.logging.log_tool_calls— off by default. Whentrue, everytools/calllogs 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.shfills this in to~/.local/state/linux-mcp/mcp_server.logfor 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--quieton the command line to suppress the stderr echo (the file is unaffected either way).logging.max_size_bytes/logging.max_backups— only apply whenlog_fileis set. The file is rotated once it reachesmax_size_bytes(default 10 MiB), keeping up tomax_backupsold copies as<log_file>.1..<log_file>.N(oldest dropped first; default 5). Setmax_backupsto0to 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.