systemd and the Linux packages¶
The .deb and .rpm on the releases page install the server as a systemd service. Installation has the install commands; this page is what comes after. The unit confines the process, which is defence in depth: authentication stays the boundary, and the security model says what an authenticated caller then reaches.
What the package installed¶
| Path | What it is | Owner |
|---|---|---|
/opt/scholar-mcp/venv |
The server, installed from PyPI at the package's own version (a release candidate installs the wheel attached to its GitHub release) | root |
/usr/lib/systemd/system/scholar-mcp.service |
The unit | root |
/etc/scholar-mcp/env.example |
Every variable, commented out, generated from the server's configuration surface; replaced by an upgrade | root |
/etc/scholar-mcp/env |
Your configuration, copied from env.example on first install, readable by root only (600), never touched by an upgrade |
root |
/var/lib/scholar-mcp |
The state directory, the one path the service may write | scholar-mcp:scholar-mcp |
The package also creates the scholar-mcp system user and group the service runs as, with /var/lib/scholar-mcp as home and no login shell.
Configure¶
Edit /etc/scholar-mcp/env. It holds deviations from the defaults only: every line arrives commented out, and the server starts on its defaults when nothing is set. Remove the # from the lines you change. The unit reads the file at start (EnvironmentFile=), so an edit needs a restart to take effect. Secrets belong here and nowhere else; the file is already mode 600.
# /etc/scholar-mcp/env
SCHOLAR_MCP_SERVER_NAME=notes
SCHOLAR_MCP_LOG_LEVEL=INFO
The unit starts scholar-mcp serve --transport http, so the server listens on SCHOLAR_MCP_HOST and SCHOLAR_MCP_PORT, 127.0.0.1:8000 unless set. To serve other machines, configure authentication first and put a reverse proxy in front; the configuration reference lists every variable.
Start and watch¶
The package installs the unit but does not enable it; starting on boot is your decision:
sudo systemctl enable --now scholar-mcp
systemctl status scholar-mcp
journalctl -u scholar-mcp -f
sudo systemctl restart scholar-mcp # after editing the env file
Under journald each record is one JSON object: the unit sets no log format, so the server picks its JSON renderer because journald is not a terminal. SCHOLAR_MCP_LOG_FORMAT=rich in the env file switches to the coloured renderer. Restart=on-failure brings a crashed server back after five seconds.
What the unit allows¶
The unit runs the server as the scholar-mcp user with these directives, among others:
| Directive | Effect |
|---|---|
ProtectSystem=strict, ProtectHome=yes, PrivateTmp=yes |
The filesystem is read-only except the paths listed below; /home, /root and /run/user are not visible; a private /tmp |
StateDirectory=scholar-mcp, ReadWritePaths=/var/lib/scholar-mcp |
The one writable path, created and owned for the service |
NoNewPrivileges=yes, an empty CapabilityBoundingSet= |
No privilege escalation and no capabilities |
PrivateDevices=yes, RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 |
No devices; only Unix, IPv4 and IPv6 sockets, so outbound connections to other services work |
SystemCallFilter=@system-service |
Only the system calls an ordinary service needs |
MemoryDenyWriteExecute=no |
Left open because Python needs writable, executable memory |
UMask=0027 |
New files are group-readable, so an administrator in the scholar-mcp group can inspect the state directory |
To let the server read or write a path outside /var/lib/scholar-mcp, add a drop-in rather than editing the unit, which the next package upgrade replaces:
sudo systemctl edit scholar-mcp
[Service]
ReadWritePaths=/srv/scholar-mcp
systemctl edit reloads the unit definitions; restart the service afterwards. The other directives stay as they are. On a host with SELinux enforcing, the directory also needs a label the service may use (semanage fcontext, then restorecon).
Upgrade¶
Install the new package the same way as the first one. Its postinstall installs the new version into the existing virtual environment and restarts the service when it is running. /etc/scholar-mcp/env is left alone; env.example is replaced, so compare the two for variables the release added:
diff /etc/scholar-mcp/env.example /etc/scholar-mcp/env
What a release changes for your clients and your data is on the Upgrade page.
Without a package¶
On a distribution the packages do not cover, mirror what the package does. The unit and the environment template live in the repository's packaging/ directory at every release tag. Before you run the block, set EXTRAS to the value packaging/scripts/postinstall.sh gives it at the same tag: the package installs those extras, and an install without them lacks the features they bring.
sudo groupadd --system scholar-mcp
sudo useradd --system --gid scholar-mcp --no-create-home \
--home-dir /var/lib/scholar-mcp --shell /usr/sbin/nologin scholar-mcp
sudo mkdir -p /opt/scholar-mcp /etc/scholar-mcp /var/lib/scholar-mcp
sudo python3 -m venv /opt/scholar-mcp/venv
EXTRAS="" # set from packaging/scripts/postinstall.sh first, such as "[all]"
sudo /opt/scholar-mcp/venv/bin/pip install "pvliesdonk-scholar-mcp${EXTRAS}==X.Y.Z"
sudo curl -fsSL -o /usr/lib/systemd/system/scholar-mcp.service \
https://raw.githubusercontent.com/pvliesdonk/scholar-mcp/vX.Y.Z/packaging/scholar-mcp.service
sudo curl -fsSL -o /etc/scholar-mcp/env.example \
https://raw.githubusercontent.com/pvliesdonk/scholar-mcp/vX.Y.Z/packaging/env.example
sudo cp /etc/scholar-mcp/env.example /etc/scholar-mcp/env
sudo chmod 600 /etc/scholar-mcp/env
sudo chown scholar-mcp:scholar-mcp /var/lib/scholar-mcp
sudo systemctl daemon-reload
Then configure and start as above. Python 3.11 or newer with the venv module is the one system requirement.
If it does not start¶
journalctl -u scholar-mcp -n 50 --no-pagershows the last records; a configuration error names the variable.- Run the command as the service user to see the error directly:
sudo -u scholar-mcp /opt/scholar-mcp/venv/bin/scholar-mcp serve --transport http. - A permission error on a path outside
/var/lib/scholar-mcpis the confinement above: add the drop-in, or move the data. systemd-analyze verify /usr/lib/systemd/system/scholar-mcp.servicechecks the unit after an edit.
Set SCHOLAR_MCP_CACHE_DIR in /etc/scholar-mcp/env before the first start. Its default, /data/scholar-mcp, is the container path, and the unit's confinement leaves only the state directory writable:
SCHOLAR_MCP_CACHE_DIR=/var/lib/scholar-mcp
# Optional: a Semantic Scholar API key for its own allowance instead of the shared pool
SCHOLAR_MCP_S2_API_KEY=your-key-here
The cache, downloaded PDFs and converted Markdown all live under that directory, so no other path needs ReadWritePaths. The service reaches out to Semantic Scholar, OpenAlex, CrossRef, Open Library and Google Books for metadata; to arXiv, PubMed Central and Unpaywall for open-access PDFs; to the standards bodies' catalogues, and to GitHub for the Tier 2 standards sync; and, when configured, to EPO OPS and your docling-serve. A firewall that limits outbound traffic has to allow those.