Skip to content

Latest commit

 

History

History
123 lines (97 loc) · 6.42 KB

rest-api.md

File metadata and controls

123 lines (97 loc) · 6.42 KB

REST API, event stream and MCP

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

Where the API is described

  • GET /api/v1/openapi.json — the OpenAPI 3.1 document, generated by the running daemon. A copy is checked in as docs/openapi.json; a test fails when it is stale (EMULE_WRITE_OPENAPI=1 ./tst_WebServer api_checkedInOpenApiIsCurrent rewrites 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.css in 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.

Conventions

  • Key: X-Api-Key: <key> or Authorization: Bearer <key>. /health needs 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": true in the body, or ?confirm=true on DELETE.
  • Version: every response carries X-Contract-Version; GET /capabilities lists the features and every operation id.

Events

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=.

Preferences

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.

MCP

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).

For contributors

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.