The daemon's web server (default port 4711) serves three things for programs, each with its own switch in Options → Web Interface:
| Surface | Path | Switch (preferences.yml, webserver:) |
Default |
|---|---|---|---|
| REST API | /api/v1/* |
restApiEnabled |
off |
| MCP endpoint for AI assistants | /mcp |
mcpEnabled (+ mcpReadOnly) |
off |
| Template web UI | / |
enabled |
off |
They share the port, the listen address, HTTPS and the API key (apiKey). Listening on all
interfaces, the server does not start when another program already uses the port on one local
address (127.0.0.1, for instance): requests to that address would reach that program. The log
names the address. When a surface is on, the daemon logs its address at start:
WebServer: REST API playground at http://localhost:4711/api/v1/docs
WebServer: OpenAPI document at http://localhost:4711/api/v1/openapi.json
WebServer: MCP endpoint at http://localhost:4711/mcp
GET /api/v1/openapi.json— the OpenAPI 3.1 document, generated by the running daemon. A copy is checked in asdocs/openapi.json; a test fails when it is stale (EMULE_WRITE_OPENAPI=1 ./tst_WebServer api_checkedInOpenApiIsCurrentrewrites it).GET /api/v1/docs— Swagger UI on that document. Press Authorize, paste the API key, and every route can be tried from the page. The page and its two files are served by the daemon itself (webserver/swagger-ui-bundle.js,swagger-ui.cssin the config directory); nothing is loaded from the internet.
Neither needs a key: a browser cannot send a header for a page load, and they hold no secrets. Every call does.
- Key:
X-Api-Key: <key>orAuthorization: Bearer <key>./healthneeds none. - Errors:
{"error": {"code": <HTTP status>, "message": "..."}}, also for an unknown route. - Strict input: an unknown query parameter or body field is a
400, as is a value of the wrong type or out of range. Nothing is ignored silently. - Lists:
{items, total, offset, limit};?limit=1–1000 (default 100),?offset=. - Destructive calls (cancel a download, remove a Usenet release or a category, shut down)
need
"confirm": truein the body, or?confirm=trueonDELETE. - Version: every response carries
X-Contract-Version;GET /capabilitieslists the features and every operation id.
GET /api/v1/events is a text/event-stream. Most events say only that a list changed —
fetch it again:
| Event | Data |
|---|---|
downloads.changed, uploads.changed, shared.changed, friends.changed, categories.changed, usenet.changed, search.results |
none |
downloads.completed |
hash, fileName |
connection.changed |
connected, connecting, lowID, firewalled, netBlocked, server |
kad.changed |
running, connected, firewalled |
nat.changed |
statusText, methodText, externalAddress |
search.state |
searchID, state, hasMore |
usenet.finished |
id, success, message |
sync.reset |
the gap could not be replayed: fetch everything again |
?topics=downloads,search limits the stream to those prefixes. Last-Event-ID (or
?lastEventId=) resumes from the daemon's ring of the last 1,000 events. A comment line
arrives every 15 seconds. A "changed" event is sent at once and then at most once a second
per type. Statistics, log lines and chat are not pushed; poll /stats and /logs?since=.
GET /api/v1/preferences/schema lists every preference the API offers with type, range, unit,
default, restartRequired and writable. PATCH /api/v1/preferences checks the whole body
against it before changing anything, then stores through the same code a GUI save runs.
Credentials, API keys and anything naming a program or certificate are not in the schema and
cannot be read or set here; the web server's own switches are shown but read-only.
POST /mcp speaks the Model Context Protocol over streamable HTTP, stateless, tools only, in
protocol revision 2026-07-28 and no other. There is no initialize handshake: every request
names the revision in params._meta["io.modelcontextprotocol/protocolVersion"] and mirrors it,
the method and the tool name into the MCP-Protocol-Version / Mcp-Method / Mcp-Name
headers. server/discover describes the server. One message per POST; GET and DELETE
answer 405.
A client of an older revision (one that opens with initialize, or names no revision) gets
HTTP 400 with the JSON-RPC error -32022 listing the supported revision, so it can say what is
wrong instead of failing silently. Headers that are missing or disagree with the body are
-32020 (400); an unknown method is -32601 (404).
The tools are operations of the same table as the REST routes — same validation, same code —
chosen for what an assistant needs (status, search, downloads, servers, Kad, shared files,
categories, preferences, logs, Usenet). The OpenAPI document marks them with x-mcp-tool.
Shutting the daemon down is not a tool. With mcpReadOnly only the tools that change nothing
are offered. A request with an Origin header that is neither this server nor on
corsAllowedOrigins is refused.
A client that speaks HTTP (Claude Code, for example):
claude mcp add --transport http emuleqt http://localhost:4711/mcp --header "Authorization: Bearer <api key>"
A client that only starts a local program uses the bridge emuleqt-mcp, which reads the
port and key from the local configuration and adds the headers the HTTP transport needs. It
ships next to the daemon: in the folder of the Linux tarball and the Windows zip, and in
Contents/MacOS/ of the app bundle on macOS. The client itself has to speak 2026-07-28.
{ "mcpServers": { "emuleqt": { "command": "/path/to/emuleqt-mcp" } } }For a daemon on another machine: emuleqt-mcp --url http://host:4711/mcp --api-key <key>
(or EMULEQT_API_KEY; --insecure accepts a self-signed certificate, --config <dir> picks
another configuration directory).
A route exists only as an entry of the table in src/core/webserver/WebServerApi.cpp
(api::Operation: path, parameters with type and range, flags, handler). Routing, validation,
the OpenAPI document and the MCP tool list are generated from it. Actions go through
src/core/app/CoreOps.h, which the IPC handlers call too; what only the daemon can do sits
behind ApiBackend.