Configuration
arcane-mcp reads its configuration from three places. In increasing order of precedence:
- a
.envfile in the working directory (KEY=valueper line;#comments and anexportprefix are accepted; it never overrides a variable already set), - the process environment,
- command-line flags.
The server refuses to start, with every problem listed, when ARCANE_URL is missing, the tool surface is not one of the three, a value cannot be read (an on/off switch, a number or a duration; on stdio only the settings stdio reads count, see Notes), a limit or timeout is out of range, a trusted origin is malformed, a stray argument follows the flags, or — in stdio mode — there is neither an API key nor a username and password. arcane-mcp --help lists every flag and variable with its default.
Reference
Section titled “Reference”| Variable | Flag | Default | Meaning |
|---|---|---|---|
ARCANE_URL | --arcane-url | — | Arcane server URL (required), or several as name=url,name=url (see Several instances) |
ARCANE_API_KEY | — | — | API key, sent as X-API-Key (recommended). With several instances, one per instance as name=key,name=key |
ARCANE_USERNAME, ARCANE_PASSWORD | — | — | Session login instead of, or in addition to, an API key. Arcane accepts only a logged-in session for a few account operations (passkeys, MFA); with both configured, those use the session and everything else the key |
ARCANE_ENVIRONMENT_ID | --environment | 0 | Environment used when an action’s environmentId is omitted (0 is Arcane’s local Docker). With several instances, one value for all or name=id,... per instance (unlisted ones get 0) |
ARCANE_MCP_INSTANCE | --instance | — | With several instances in ARCANE_URL, the one a stdio server serves (required then); over HTTP clients pick with X-Arcane-URL, so setting it stops the server |
ARCANE_SKIP_TLS_VERIFY | --skip-tls-verify | false | Accept self-signed certificates |
ARCANE_MCP_TOOL_SURFACE | --tool-surface | dynamic | dynamic, meta or individual |
ARCANE_MCP_READ_ONLY | --read-only | false | Expose only read-only actions |
ARCANE_MCP_SAFE_MODE | --safe-mode | false | Answer mutating calls with a preview |
ARCANE_MCP_EXCLUDE_TOOLS | --exclude-tools | — | Comma-separated action IDs (image.prune), domains (swarm_node) or tool names to hide. An entry that matches nothing is logged as a warning at startup |
ARCANE_MCP_MAX_DOWNLOAD_BYTES | — | 4194304 | Cap for downloads and collected stream output |
ARCANE_MCP_STDIO_MAX_LINE_BYTES | — | 16777216 | Largest stdio message (one line); a longer one is answered with an error and the session goes on. The default takes one upload.chunk_put at Arcane’s 10 MiB chunk, base64-encoded, as ARCANE_MCP_HTTP_MAX_BODY_BYTES does over HTTP |
ARCANE_MCP_TIMEOUT | — | 60s | Timeout of one non-streaming request |
ARCANE_MCP_ACTION_TIMEOUT | --action-timeout | 30m, or ARCANE_MCP_TIMEOUT when that is longer | Overall deadline of one action, progress streams and downloads included. An explicit value shorter than ARCANE_MCP_TIMEOUT stops the server at startup (see Safety). Also bounds how long a stdio server whose stdin closed waits for the answers still owed |
ARCANE_MCP_ELICITATION | --elicitation | true | Ask the user to confirm a destructive call without confirm: true through MCP elicitation when the client supports it; off always refuses instead (see Safety) |
ARCANE_MCP_ELICITATION_TIMEOUT | --elicitation-timeout | 5m | How long to wait for the user’s answer before refusing |
ARCANE_MCP_AUDIT_LOG | --audit-log | true | One info log record per tool call (see Safety) |
ARCANE_MCP_AUDIT_LOG_ARG_NAMES | --audit-log-arg-names | false | Add the names of the arguments passed (never their values) to the audit records |
ARCANE_MCP_HTTP_ADDR | --http | — | Serve Streamable HTTP on this address (path /mcp) instead of stdio |
ARCANE_MCP_HTTP_TOKEN | — | — | Bearer token HTTP clients must present. Without it, HTTP is for trusted networks only (see HTTP mode). Deliberately no flag: a command line is visible to every user of the host |
ARCANE_MCP_HTTP_MAX_API_KEYS | --http-max-api-keys | 100 | Distinct Arcane API keys (X-Arcane-API-Key) served at once; each one has its own MCP server |
ARCANE_MCP_HTTP_API_KEY_IDLE_TIMEOUT | --http-api-key-idle-timeout | 10m | Reclaim a key’s server this long after its last session ended and its last request finished (0: only when a new key needs the room) |
ARCANE_MCP_HTTP_SESSION_IDLE_TIMEOUT | --http-session-idle-timeout | 30m | Close a session after this long without a request (0: never) |
ARCANE_MCP_HTTP_MAX_SESSIONS | --http-max-sessions | 1000 | Live sessions over every key |
ARCANE_MCP_HTTP_MAX_CONCURRENT_REQUESTS | --http-max-concurrent-requests | 64 | POST and DELETE requests served at once over every client (event streams are not counted) |
ARCANE_MCP_HTTP_MAX_BODY_BYTES | --http-max-body-bytes | 16777216 | Largest request body (16 MiB: one upload.chunk_put at Arcane’s default 10 MiB chunk, base64-encoded) |
ARCANE_MCP_HTTP_TRUSTED_ORIGINS | --http-trusted-origins | — | Comma-separated browser origins (https://app.example.com) allowed to call /mcp cross-origin; * allows any and turns the protection off |
ARCANE_MCP_HTTP_ALLOWED_HOSTS | --http-allowed-hosts | — (any) | Comma-separated Host values /mcp answers: a host name (mcp.example.com, any port) or host:port. Set it whenever the server listens beyond loopback without a token (DNS rebinding, see HTTP mode) |
ARCANE_MCP_HTTP_STATELESS | --http-stateless | false | Serve stateless Streamable HTTP, the transport of protocol revision 2026-07-28: no sessions, every POST stands alone, GET and DELETE answer 405 (see Stateless HTTP) |
ARCANE_MCP_HTTP_AUTH | --http-auth | token | How HTTP clients authenticate: token (the Bearer is ARCANE_MCP_HTTP_TOKEN, the Arcane key travels in X-Arcane-API-Key or comes from the configuration) or apikey (the Bearer is the caller’s own Arcane API key, checked against Arcane). apikey with ARCANE_MCP_HTTP_TOKEN, ARCANE_API_KEY, ARCANE_USERNAME or ARCANE_PASSWORD stops the server at startup (see HTTP authentication) |
ARCANE_MCP_HTTP_AUTH_CACHE_TTL | --http-auth-cache-ttl | 5m | apikey mode: how long a key Arcane accepted is served before Arcane is asked again |
ARCANE_MCP_HTTP_AUTH_MAX_FAILURES | --http-auth-max-failures | 10 | Failed authentications (a wrong token, a key Arcane refuses) one client address may make per window before it gets 429 for the rest of it (0: no limit) |
ARCANE_MCP_HTTP_AUTH_FAILURE_WINDOW | --http-auth-failure-window | 1m | The window of ARCANE_MCP_HTTP_AUTH_MAX_FAILURES, counted from the first failure |
ARCANE_MCP_HTTP_AUTH_NETWORK_FACTOR | --http-auth-network-factor | 10 | An IPv6 /48 is refused with 429 once it has had this many times ARCANE_MCP_HTTP_AUTH_MAX_FAILURES credentials refused within one window, across its /64s (0: no /48 limit; no effect when ARCANE_MCP_HTTP_AUTH_MAX_FAILURES is 0) |
ARCANE_MCP_HTTP_TRUSTED_PROXIES | --http-trusted-proxies | — | Comma-separated proxy addresses or CIDR ranges (10.0.0.0/8, ::1) whose ARCANE_MCP_HTTP_TRUSTED_PROXY_HEADER names the client address. Set both or neither |
ARCANE_MCP_HTTP_TRUSTED_PROXY_HEADER | --http-trusted-proxy-header | — | Header those proxies put the client address in (X-Forwarded-For, X-Real-IP); Forwarded is not supported |
ARCANE_MCP_HTTP_RATE_LIMIT_RPS | --http-rate-limit-rps | 0 (off) | Sustained requests per second of one Arcane key (a decimal such as 0.5 is fine); past it the key gets 429 with Retry-After |
ARCANE_MCP_HTTP_TLS_CERT, ARCANE_MCP_HTTP_TLS_KEY | --http-tls-cert, --http-tls-key | — | Serve HTTPS (TLS 1.2 at least) with this PEM certificate (chain) and key; both or neither. Unreadable or mismatched files stop the server; a renewed pair is picked up without a restart (see Listener, probes and shutdown) |
ARCANE_MCP_HTTP_SOCKET_MODE | --http-socket-mode | 0660 | Permission (octal) of the unix socket when ARCANE_MCP_HTTP_ADDR is a path (/run/arcane-mcp.sock) or unix:<path>; on Windows the directory’s ACL decides (see below) |
ARCANE_MCP_HTTP_DRAIN_DELAY | --http-drain-delay | 0s | After SIGTERM, how long /readyz answers 503 while the server keeps serving, before it stops accepting connections (at most 5m) |
ARCANE_MCP_HTTP_ALLOW_PLAINTEXT | --http-allow-plaintext | — (unset) | What becomes of a TCP listener other machines can reach that serves plain HTTP: unset, it is served and startup warns; true (or the flag alone) declares that something in front encrypts the hop, logged at INFO; false refuses to start it. TLS, loopback and a unix socket are never affected. See Listener, probes and shutdown |
ARCANE_MCP_HTTP_RATE_LIMIT_BURST | --http-rate-limit-burst | 40 | Requests one key may make at once before the rate paces it (at least 1 when the rate is set) |
ARCANE_MCP_HTTP_SERVER_CARD | — | true | Serve the server card at /server-card and /mcp/server-card |
ARCANE_MCP_HTTP_DISCOVERY_RATE_LIMIT_RPS | --http-discovery-rate-limit-rps | 20 | Requests a second one client address may make for the server card (0: no limit; at most 1000) |
ARCANE_MCP_HTTP_DISCOVERY_RATE_LIMIT_BURST | --http-discovery-rate-limit-burst | 200 | Server card requests one client address may make at once (0: no limit; at most 10000) |
ARCANE_MCP_ICONS | — | true | Send the brand icon with the server and every tool, prompt and resource; off sends none, for a client that refuses data: URI icons (see Icons) |
ARCANE_MCP_OUTPUT_SCHEMAS | — | false | Experimental: on the individual surface, declare each action’s output schema, derived from Arcane’s OpenAPI document (see Output schemas) |
ARCANE_MCP_LOG_LEVEL | --log-level | info | debug, info, warn or error. Logs go to stderr only. Any other value stops the server at startup |
LOG_LEVEL | — | — | Deprecated alias of ARCANE_MCP_LOG_LEVEL, read only when that is unset, with a warning at startup. A value that is not a level is ignored with a warning (other software may use this name) |
The on/off switches (ARCANE_SKIP_TLS_VERIFY, ARCANE_MCP_READ_ONLY, ARCANE_MCP_SAFE_MODE, ARCANE_MCP_ELICITATION, ARCANE_MCP_AUDIT_LOG, ARCANE_MCP_AUDIT_LOG_ARG_NAMES, ARCANE_MCP_HTTP_STATELESS, ARCANE_MCP_ICONS, ARCANE_MCP_OUTPUT_SCHEMAS) accept true/false, 1/0, yes/no and on/off; any other value stops the server at startup, so a mistyped safety switch never leaves it writing.
arcane-mcp --version prints the version, commit and build date and exits. arcane-mcp --help prints every flag and variable with its default and exits with status 0, whatever the environment holds.
Behaviour worth knowing
Section titled “Behaviour worth knowing”Behaviour that a habit from other servers, or a quick reading of the table, may not lead you to expect. What each release changes is in the changelog.
- A variable set to an empty or blank value counts as unset, for every setting: it gets the default, and a
.envfile can fill it. Desktop extensions and registry clients send an empty value for a field the user cleared, so an emptyARCANE_MCP_TOOL_SURFACEis the default surface and an emptyARCANE_ENVIRONMENT_IDis environment0. - The container image has a
HEALTHCHECK(arcane-mcp --probe, see Listener, probes and shutdown); a stdio container probes healthy. --help(-h) exits with status 0, and works even when the environment holds a value the server would refuse.- A malformed number or duration (
ARCANE_MCP_TIMEOUT=60,ARCANE_MCP_MAX_DOWNLOAD_BYTES=4MB) stops the server at startup with status 2 instead of falling back to the default.ARCANE_MCP_TIMEOUTmust also be positive. - An argument left over after the flags (
--read-only false) is an error, so no flag after it is silently dropped. - Destructive calls without
confirm: trueask the user through MCP elicitation on clients that support it (ARCANE_MCP_ELICITATION), and every tool call writes an audit record (ARCANE_MCP_AUDIT_LOG). - Each action runs under an overall deadline (
ARCANE_MCP_ACTION_TIMEOUT):30m, orARCANE_MCP_TIMEOUTwhen that is longer. It also bounds streamed progress (image pulls and builds, project up and redeploy) and downloads; one that runs longer is cancelled with an error naming the deadline. - The log level is
ARCANE_MCP_LOG_LEVEL, like every other setting.LOG_LEVELis read whenARCANE_MCP_LOG_LEVELis unset, with a deprecation warning; aLOG_LEVELvalue that is not a level is ignored with a warning. An unreadableARCANE_MCP_LOG_LEVELor--log-levelstops the server at startup. - An
ARCANE_MCP_EXCLUDE_TOOLSentry that matches no action, domain or tool name (a typo, or an alias such as an OpenAPI operation ID, which exclusion does not read) is logged as a warning at startup; it hides nothing. - Stateless Streamable HTTP is available with
ARCANE_MCP_HTTP_STATELESS(off by default, see Stateless HTTP). - Over HTTP, a client address that sends 10 wrong tokens within a minute gets
429withRetry-Afteruntil the minute ends (ARCANE_MCP_HTTP_AUTH_MAX_FAILURES,0turns it off); behind a reverse proxy setARCANE_MCP_HTTP_TRUSTED_PROXIESandARCANE_MCP_HTTP_TRUSTED_PROXY_HEADER, or every client shares the proxy’s budget.ARCANE_MCP_HTTP_AUTH=apikeyand a per-key rate limit are off by default (see HTTP authentication). - The auth-failure budget also blocks an IPv6
/48as a whole once it has had ten timesARCANE_MCP_HTTP_AUTH_MAX_FAILUREScredentials refused within one window (see HTTP authentication). The factor isARCANE_MCP_HTTP_AUTH_NETWORK_FACTOR(10;0turns the/48limit off). - Two trusted-proxy settings that cannot work stop the server at startup: a unix-socket
ARCANE_MCP_HTTP_ADDRwhoseARCANE_MCP_HTTP_TRUSTED_PROXIESlists addresses but notunix(a socket peer has no address, so the header would never be believed), andunixin that list on a TCP listener. Each would charge every client to one shared budget. See the pairing table in HTTP deployment. - On Windows, an
ARCANE_MCP_HTTP_ADDRthat contains\is a unix socket path, as one that contains/is everywhere. - Safe-mode previews never echo credential-like arguments: passwords, keys, tokens and a secret’s
dataare shown as(hidden)in the parameters and the JSON body, as in the confirmation question. - Result content carries MCP audience annotations: the text block is marked for the assistant (
audience: ["assistant"]) and a binary download for the user (audience: ["user"]). A client that filters content by audience may not show the text to the user, or may not pass downloads to the model. See Output format. /readyz?format=jsondoes not show the exact version each Arcane instance reports: it asks for no credential, and the version would let anyone who reaches the server fingerprint the Arcane behind it. It showsversionMatchandcatalogVersion; the exact version is in the startup log line.- A stdio server given an HTTP-only setting (
ARCANE_MCP_HTTP_TOKEN,ARCANE_MCP_HTTP_RATE_LIMIT_RPS, TLS, socket or proxy settings…) logs a warning naming them at startup instead of ignoring them silently. See Notes.
- Booleans accept anything Go’s
strconv.ParseBooldoes (1,true,TRUE,0,false…) plusyes/noandon/off. Numbers are whole numbers of bytes or items (16777216, not16MiB). An unreadable value of either, or of a duration, stops the server at startup rather than falling back to a default nobody chose; an empty value is the default. - Durations use Go syntax with a unit:
90s,2m,1m30s(a bare60is refused). ARCANE_ENVIRONMENT_IDis a default, not a restriction. Every action whose Arcane route lives under/environments/{id}/takes an optionalenvironmentIdparameter; omitted, it is this value. Useenvironment.listto find the IDs.ARCANE_MCP_EXCLUDE_TOOLSmatches canonical IDs (container.kill), domains (swarm_node), meta tool names (arcane_swarm_node) and individual tool names (arcane_container_kill), exactly (case matters). An entry that matches none of them hides nothing and is named in a startup warning. See Safety.ARCANE_MCP_TIMEOUTbounds ordinary requests. Progress streams (image pull and build, project up, pull, build and redeploy) are read to the end, withinARCANE_MCP_ACTION_TIMEOUT, which bounds every action as a whole; the live-stream actions have their ownsecondsparameter; see Streams and files.- HTTP mode does not require a server-wide credential: clients can bring their own Arcane key in
X-Arcane-API-Key. See HTTP mode and Safety. - stdio reads one JSON-RPC message per line. A line that is not one (not JSON, JSON that is not a message, a batch the negotiated protocol revision does not allow, a line over
ARCANE_MCP_STDIO_MAX_LINE_BYTES, 16 MiB by default, or a message nesting JSON more than 100 levels deep, a batch’s own array not counted, which is refused before it is decoded) is answered with a JSON-RPC error (-32700or-32600) and the session goes on. Closing stdin ends the process with status 0 once the requests already sent are answered, soecho '<request>' | arcane-mcpand scripts that close their end after writing get their answers; the wait is bounded byARCANE_MCP_ACTION_TIMEOUT, and a signal still ends the process within 2 seconds. Started by hand in a terminal (stdin is a terminal, which an MCP client never connects), it first writes one line to stderr saying that it is an MCP server for a client to start, and where client setup is explained. - HTTP-only settings on stdio. A stdio server ignores every
ARCANE_MCP_HTTP_*setting other thanARCANE_MCP_HTTP_ADDR(the token, limits, TLS, socket, proxy and auth-budget settings). When any of them is set, in the environment, a.envfile or a flag, it logs one warning at startup naming them (never their values):stdio mode ignores HTTP-only settings, which apply only with ARCANE_MCP_HTTP_ADDR (--http): …. The server starts as usual, even when such a value could not be read or is out of range (ARCANE_MCP_HTTP_RATE_LIMIT_RPS=abc,ARCANE_MCP_HTTP_SESSION_IDLE_TIMEOUT=-5s): those values are checked only by an HTTP server, which still refuses to start on them. A flag is different: a malformed value on the command line (--http-rate-limit-rps abc) stops either server, since the command line is read before the server knows which it is.
HTTP mode
Section titled “HTTP mode”--http (or ARCANE_MCP_HTTP_ADDR) serves Streamable HTTP at /mcp, plus two probes that never ask for the token:
| Path | Answers |
|---|---|
/healthz | Liveness: 200 ok while the process serves, whatever the state of Arcane |
/readyz | Readiness: 200 ready when Arcane answers its health check (GET /api/health, no credential needed) and the server is not shutting down; 503 not ready: … otherwise. The check is cached for 5 seconds, so probes cannot flood Arcane. ?format=json adds Arcane’s version and the catalog’s (fields); a version that differs only logs a warning and never makes it fail |
What the transport guards, with or without a token:
- One server per Arcane key, bounded. Each distinct
X-Arcane-API-Keygets its own MCP server. At mostARCANE_MCP_HTTP_MAX_API_KEYSare kept; a key’s server is reclaimed only when it has no live session and no request in flight (afterARCANE_MCP_HTTP_API_KEY_IDLE_TIMEOUT, or at once when a new key needs the room), never by ending someone’s session. When every server is in use, a new key gets503withRetry-Afterand a JSON-RPC error naming the limit. - A session stays bound to its key. A request on a session that carries another
X-Arcane-API-Keythan the one it was opened with (or none, when it was opened with one) is refused with403; the session itself goes on. A client must send the same key on every request. - Bounded sessions and requests. At most
ARCANE_MCP_HTTP_MAX_SESSIONSsessions live at once, a session idle forARCANE_MCP_HTTP_SESSION_IDLE_TIMEOUTis closed (its next request gets404, the signal to initialize again), at mostARCANE_MCP_HTTP_MAX_CONCURRENT_REQUESTScalls run at once, and a body overARCANE_MCP_HTTP_MAX_BODY_BYTESgets413. A body with a message nesting JSON more than 100 levels deep (a batch’s own array is not counted) gets400with a JSON-RPC error-40000, the request’sidwhen it comes before the deep part, and a closed connection; it is measured as it is read, after authentication, and never decoded. A request over a cap is refused at once with503andRetry-After, never left waiting. - Allowed hosts. With
ARCANE_MCP_HTTP_ALLOWED_HOSTSset,/mcpanswers only a request whoseHostis listed (403otherwise), on any listener; the list then replaces the SDK’s loopback-only check, so a reverse proxy on the same machine may forward a public name./healthzand/readyzare not restricted. - Cross-origin protection. A request a browser makes on behalf of another site (by
Sec-Fetch-Site, or anOriginnaming another host) is refused with403on every method, unless its origin is inARCANE_MCP_HTTP_TRUSTED_ORIGINS, which also gets the CORS answers a browser needs. Clients that send noOrigin(every non-browser MCP client) are not affected. The SDK also refuses a request that reaches a loopback address with a non-loopbackHost(DNS rebinding). - No body read before authentication. The refusals of a missing token, a foreign origin or a foreign host answer at once without reading the body, with a
nullid. Later refusals read at most 4 KiB of the body, for at most a second, to answer with the request’sid. The whole request must arrive within two minutes (headers within ten seconds); answers and event streams are not time-limited. - JSON-RPC errors. Every refusal is a JSON-RPC error body, with the request’s
idwhen it was read: the standard-32700,-32600and-32601when the message itself is at fault, otherwise the HTTP status times −100 (-40100for a missing token,-40300,-42900,-50300…). The404for an unknown session stays plain, as clients expect.
Several instances
Section titled “Several instances”ARCANE_URL may list several Arcane managers as name=url pairs, such as ARCANE_URL=home=https://nas.lan:3552,lab=https://lab.example.com. A single bare URL works as before. Names are lowercase letters, digits, - and _; names and URLs must be distinct, and every listed URL must be http or https, with no credentials (user:password@), query or fragment.
- Over HTTP every request names its instance in the
X-Arcane-URLheader: a listed name (home) or exactly a listed URL (a trailing slash does not matter). A missing header is refused with400, an unlisted value with403; both are JSON-RPC errors answered before the body is read and before anything is built, and the value is never echoed. The server never sends a request to a URL its configuration does not list, and never follows a redirect from Arcane to another scheme, host or port (a redirect within the same origin is followed). Inapikeymode, where the instance is checked before the key, an unlisted name counts against the client address’s auth-failure budget, so the configured names cannot be listed by trying. With one instance the header is optional, and must name it when sent. - Each instance is separate. The same Arcane key on two instances gets two servers, two auth-cache entries and two rate-limit buckets; a session is bound to its instance as it is to its key (
403when used with another). Inapikeymode the key is checked withGET /api/auth/meon the instance the request names. The auth-failure budget stays per client address, across instances: it counts the client’s failures, wherever they are aimed. - Credentials.
ARCANE_API_KEYmust then be aname=keylist (a single key would be sent to every instance); an instance without a key serves only clients that bring their own.ARCANE_USERNAME/ARCANE_PASSWORDserve one instance only, so with several over HTTP they stop the server at startup.ARCANE_ENVIRONMENT_IDmay be one value for all or aname=idlist. - stdio serves one instance: with several listed,
ARCANE_MCP_INSTANCE(or--instance) must name it; its key, login and environment are used as with a single URL. /readyzchecks every instance and lists them by name (never by URL):200 readywhen all answer,200 degradedwhen some do (the server still serves the others’ clients, and a503would take it out of a load balancer for all of them),503when none does. Each failed check is logged. The JSON form (?format=json) lists each instance’sname,statusandversionMatchininstances./readyzasks for no credential, so anyone who reaches it learns the instance names (not their URLs) and whether each one’s Arcane version matches the catalog’s, never the exact version (only the log has it).- The audit log names the instance of every call (
instance=home); a single bare URL has no name and adds nothing.
HTTP authentication
Section titled “HTTP authentication”ARCANE_MCP_HTTP_AUTH picks who may use /mcp. In both modes the Bearer scheme is matched in any case (bearer works too).
token(default, the behaviour of earlier releases): withARCANE_MCP_HTTP_TOKENevery request needsAuthorization: Bearer <token>; each client brings its Arcane key inX-Arcane-API-Keyor runs as the server’sARCANE_API_KEYor login. A client that sends the server’s ownARCANE_API_KEYis served exactly as one that sends no key (that key and the login); any other key never gets the login. A remote user then holds two secrets.apikey: the Bearer token is the caller’s Arcane API key, so a client that can only setAuthorization(many remote connectors,mcp-remote) needs nothing else. The first request with a key asks ArcaneGET /api/auth/mewith it (a read every user may make of itself; Arcane only notes the key’s last use). The key is accepted only when Arcane answers200with its user; it is then served forARCANE_MCP_HTTP_AUTH_CACHE_TTLwithout asking again, and forgotten at once when a call with it gets401from Arcane (a key revoked meanwhile). A key Arcane refuses (401, or an environment access token, which Arcane accepts inX-API-Keybut which is no user’s key) gets401withWWW-Authenticate: Bearer error="invalid_token". Any other answer (Arcane unreachable, a403or a redirect from something in front of it, a page that is not Arcane’s) gets503withRetry-After, and nothing is remembered. There is no fallback to a server credential, soARCANE_MCP_HTTP_TOKEN,ARCANE_API_KEYand the login are refused at startup in this mode.X-Arcane-API-Keyis not needed; if a client sends it anyway it must be the same key as the Bearer (400otherwise). Sessions are bound to the Bearer key, as they are toX-Arcane-API-Keyin token mode. Use TLS in front: the Bearer is an Arcane credential.
Settings a mode would ignore stop the server at startup: a cache TTL other than the default in token mode, and trusted proxies with the budget off.
Auth-failure budget. In both modes a wrong token, or a key Arcane refuses, counts against the client address; after ARCANE_MCP_HTTP_AUTH_MAX_FAILURES within ARCANE_MCP_HTTP_AUTH_FAILURE_WINDOW (10 per minute by default) the address gets 429 with Retry-After and a JSON-RPC error (-42900) until the window ends, without its credential being checked. Only failed authentications count: a missing credential, a refused origin or host, a full server, an oversized body or a successful request never do. In apikey mode a key Arcane already accepted keeps working from a blocked address (the answer comes from memory), so one client guessing does not lock out the others behind the same address. The client address is the connection’s peer; IPv6 clients are counted by their /64, and an IPv6 /48 is blocked as a whole, for the rest of the window, once it has had ARCANE_MCP_HTTP_AUTH_NETWORK_FACTOR (10) times ARCANE_MCP_HTTP_AUTH_MAX_FAILURES credentials refused within it, across all of its /64s (100 per minute by default), so whoever holds a /48 cannot rotate through its 65,536 budgets. ARCANE_MCP_HTTP_AUTH_NETWORK_FACTOR=0 turns the /48 limit off (each /64 keeps its own budget, and a /48 can rotate through them); a larger factor makes the block harder to reach, for others and for an attacker alike. That block also reaches the other /64s of the /48, including clients of a mobile carrier or provider that hands out one /64 (or a /56 or /60) per subscriber inside a shared /48; a few stale credentials there do not reach it, but anyone in the /48 who spends that many guesses can deny it to the others for the rest of a window. Behind a reverse proxy, every client arrives from the proxy’s address and therefore shares one budget unless ARCANE_MCP_HTTP_TRUSTED_PROXIES lists the proxy and ARCANE_MCP_HTTP_TRUSTED_PROXY_HEADER names the header it fills; the header is believed only from a listed proxy and is read from the right, skipping trusted hops. At most 4096 addresses are tracked at once; past that, the failures of every further address share one budget, and once it is spent all of those addresses are refused until it lapses (in apikey mode, keys already accepted still pass). With the /48 limit on, one /48 cannot get there, since it is blocked after factor budgets’ worth of failures; filling the table takes failures from 4096 IPv4 addresses, or from about 410 /48s, at once. At most 4096 /48s are tracked as well; a further one is not aggregated (its /64s keep their own budgets) rather than share a record that would block every IPv6 client.
Rate limit. With ARCANE_MCP_HTTP_RATE_LIMIT_RPS set, each Arcane key gets a token bucket of ARCANE_MCP_HTTP_RATE_LIMIT_BURST refilled at that rate, on the stateful and the stateless transport alike, kept until it has refilled even when the key’s server is reclaimed. Every JSON-RPC request spends one (initialize, tools/list and tools/call included; some clients also send server/discover on connect), and a batch (on the protocol revisions that allow one) spends one per request in it; notifications and responses never do, so a client’s session is never broken by a refused notification. Past the limit a request gets 429, Retry-After with the seconds until the next token, and a JSON-RPC error -42900 with the request’s id; a batch larger than the burst is refused whole, and a body that starts a batch but is not valid JSON gets 400 with -32700. Requests refused before their key is known (authentication, the caps above) spend nothing.
Listener, probes and shutdown
Section titled “Listener, probes and shutdown”- TLS. With
ARCANE_MCP_HTTP_TLS_CERTandARCANE_MCP_HTTP_TLS_KEYthe listener speaks HTTPS only, TLS 1.2 or later. The files are read at startup (a missing, unreadable or mismatched pair stops the server) and checked again at most once a second as connections arrive: a renewed certificate (certbot, cert-manager, a mounted secret) is served without a restart, and a change that cannot be loaded (a half-written file) keeps the certificate already served and is logged once. Failed handshakes (port scanners, plain HTTP sent to the HTTPS port, clients that refuse a self-signed certificate) are logged as one warning per minute with the number left out since the previous one (those left out at the end of a burst are counted a minute later), and each at debug level. TLS changes nothing about authentication: without a token the trusted-network warnings still apply. - Plain HTTP on the network. A TCP listener without TLS that other machines can reach (a wildcard bind such as
:8080or0.0.0.0:8080, which is what a container binds, a routable address, or a name that resolves to anything but loopback) logs a warning at startup, in every authentication mode: the token, the Arcane keys and every answer cross the network in the clear.ARCANE_MCP_HTTP_ALLOW_PLAINTEXT=true(or--http-allow-plaintext) declares that something in front encrypts the hop, such as a reverse proxy that terminates TLS and reaches the server over a private network or a container bridge; the line is then logged at INFO.ARCANE_MCP_HTTP_ALLOW_PLAINTEXT=falseis the hardened mode: such a listener stops the server at startup (status 1) with a message naming the address and the ways out, while TLS on the listener, a loopback address (or a name that resolves only to loopback) and a unix socket start as usual. The flag wins over the variable, but a variable that cannot be read (ARCANE_MCP_HTTP_ALLOW_PLAINTEXT=maybe) still stops an HTTP server when the flag is given too, as for every setting. - Unix socket. When
ARCANE_MCP_HTTP_ADDRis a path (it contains/, or\on Windows) orunix:<path>, the server listens on a unix socket withARCANE_MCP_HTTP_SOCKET_MODE(created owner-only, then given that mode). A socket left by an earlier run is replaced only when it is a socket nobody answers on; any other file there, or a live server, stops the startup. The socket is removed when the server stops. A socket is reached only by processes its mode lets in and never by a browser, so the trusted-network and DNS-rebinding warnings are not logged;ARCANE_MCP_HTTP_ALLOWED_HOSTSand the origin checks work as on TCP. Connections on a socket have no client address: the auth-failure budget charges them all to one key,unix, unlessARCANE_MCP_HTTP_TRUSTED_PROXIEScontainsunix(a reverse proxy on the same host) andARCANE_MCP_HTTP_TRUSTED_PROXY_HEADERnames the client. Windows 10 and later have unix sockets too (C:\ProgramData\arcane-mcp\mcp.sock, orunix:C:\...), but no permission bits: the socket takes its directory’s access control list, so put it in a directory only the intended users can open;ARCANE_MCP_HTTP_SOCKET_MODEdoes not decide who can connect there. arcane-mcp --probeasks the local listener for/healthz(/readyzwith--ready) and exits0on a200,1otherwise, within--timeout(3 s). It readsARCANE_MCP_HTTP_ADDRandARCANE_MCP_HTTP_TLS_CERTthe way the server does (a.envfile, the environment, or--http/--http-tls-certafter--probe), needs no Arcane URL or credential and never starts a server. A wildcard address (:8080,0.0.0.0,[::]) is asked at loopback; HTTPS to loopback or a socket is not verified (the probe asks its own server), elsewhere it is verified against the system roots and the configured certificate. A listener on port0cannot be found: pass the logged port with--http. Without an HTTP address (stdio mode) it says so and exits0. The container image’sHEALTHCHECKruns it: an HTTP container is healthy once/healthzanswers, and a stdio container (docker run -i) is healthy while it runs, instead of being marked unhealthy for lacking a listener it never had./healthz?format=json(orAccept: application/jsonalone; an MCP client’sapplication/json, text/event-streamkeeps the plain answer) answers{"status":"ok","draining":false,"version":…,"commit":…,"date":…,"configDigest":…}. The digest is the first 12 hex digits of a SHA-256 of the settings that shape the server (surface, read-only and safe mode, exclusions, timeouts, limits, the authentication mode, instance names…), never a credential, a URL or a path: two replicas with the same digest were started the same way. Plainokstays the default.- Draining. With
ARCANE_MCP_HTTP_DRAIN_DELAY, SIGTERM first turns/readyzto503(anddrainingtotrue) while the server keeps serving, so a load balancer stops sending new requests; after the delay the usual graceful shutdown runs. Keep the delay below the orchestrator’s stop timeout (Docker’s default is 10 s).
Server card
Section titled “Server card”Over HTTP the server publishes a server card (SEP-2127, media type application/mcp-server-card+json) at /mcp/server-card, the location the extension reserves next to the MCP endpoint, and at /server-card. A client or a registry reads it before connecting, without a credential. It holds:
- the server’s identity, as in its MCP Registry entry:
name(io.github.jmrplens/arcane-mcp),version(the running build’s),title,description,websiteUrl,repositoryandicons; - one Streamable HTTP endpoint,
{baseUrl}/mcp.baseUrlis a variable the client fills in with the address it fetched the card from, because the server cannot know the address clients reach it at (behind a proxy, a port forward or a socket); - the protocol revisions that endpoint serves (2026-07-28 only with
ARCANE_MCP_HTTP_STATELESS); - the headers a connection needs, by authentication mode, never with a value:
Authorization(required) whenARCANE_MCP_HTTP_TOKENis set or inapikeymode;X-Arcane-API-Keyin token mode, marked required only when there is no token and no instance has a key or login of its own (with a token it is listed as optional, and its description does not say whether the server has an Arcane credential);X-Arcane-URL, required with several instances when there is no token or inapikeymode, and with a token always listed as optional, so the card does not tell how many instances there are.
It never holds tools, resources, prompts or capabilities (the extension leaves those to the live listing), a credential, or anything about the Arcane instances behind the server (no URL, no instance name, no Arcane version). The build version it shows is the one /healthz?format=json already shows.
The answer carries Cache-Control: public, max-age=3600 and an ETag (derived from the card’s bytes, so replicas agree): a request with If-None-Match gets 304. Any origin may read it (Access-Control-Allow-Origin: *), as the extension asks for public metadata; OPTIONS answers the preflight, any method other than GET, HEAD and OPTIONS gets 405. ARCANE_MCP_HTTP_ALLOWED_HOSTS applies as on /mcp.
Each client address has a token bucket on these paths (ARCANE_MCP_HTTP_DISCOVERY_RATE_LIMIT_RPS, 20 a second, and ARCANE_MCP_HTTP_DISCOVERY_RATE_LIMIT_BURST, 200), whether the request is answered 200, 304 or is a preflight; behind a proxy the address is decided by ARCANE_MCP_HTTP_TRUSTED_PROXIES as for the auth-failure budget. Past it a request gets 429, Retry-After and a JSON-RPC error -42900. Either setting at 0 turns the limit off, for a proxy that already limits these paths.
It is on by default because it tells an unauthenticated caller nothing it could not learn from /mcp and /healthz already: /healthz?format=json shows the build version; without a token /mcp answers anyone, so whether a client must bring its own Arcane key is no secret; with a token /mcp answers 401, which says a token is needed, and the card then does not say whether the server holds an Arcane credential; with several instances, /mcp without a token, or in apikey mode with any Bearer, answers 400 to a request without X-Arcane-URL before it checks the key, while with a token it asks for the token first and the card then does not tell the count either. A client that discovers servers needs the card. ARCANE_MCP_HTTP_SERVER_CARD=off removes both paths (they answer 404).
Stateless HTTP
Section titled “Stateless HTTP”ARCANE_MCP_HTTP_STATELESS=on (or --http-stateless) serves the stateless Streamable HTTP transport that protocol revision 2026-07-28 requires. It is off by default: the stateful transport serves every client today, and the clients that speak 2026-07-28 fall back to 2025-11-25 on it, while the stateless one takes something away from 2025-era clients (see below). The guards of HTTP mode apply to both, except those about sessions.
| Stateful (default) | Stateless | |
|---|---|---|
| Protocol revisions | 2025-11-25 and earlier; a 2026-07-28 client is told to fall back | 2026-07-28 and earlier |
| Sessions | Mcp-Session-Id, bound to the key that opened it (403 for another key) | None: an Mcp-Session-Id sent is ignored, never refused |
| Arcane key | The one the session was opened with | Read from every POST (X-Arcane-API-Key, else the server’s key or login), so consecutive requests may use different keys |
GET (event stream), DELETE | Served | 405 with Allow: POST and a JSON-RPC error (-40500) |
| Confirmation of destructive calls | Elicitation on every revision | On 2026-07-28 the question travels in the call’s result and the client retries with the answer; a 2025-era client cannot be asked (it would need a request from the server outside any call) and gets the confirm: true refusal at once |
| Progress notifications | On the call’s own event stream | Same: they travel on the POST’s event stream while the call runs |
ARCANE_MCP_HTTP_MAX_SESSIONS, ARCANE_MCP_HTTP_SESSION_IDLE_TIMEOUT | Apply | Do not apply |
| A key’s server counts as in use | While it has a live session or a request in flight | Only while a request runs on it |
On the stateless transport a 2026-07-28 call whose client has gone away is cancelled, with its request to Arcane, since its answer can no longer be delivered. The server sends no keep-alive pings on either transport. The token, the host and origin checks, the body and concurrency limits, the key cap and the JSON-RPC refusals behave the same; the 405 is answered after the token check and without reading the body.
Examples
Section titled “Examples”A read-only assistant on the meta surface, pinned to one remote environment:
ARCANE_URL=https://arcane.example.com \ARCANE_API_KEY=arc_... \./arcane-mcp --tool-surface meta --read-only --environment 6791a839-...A browser-based client on another origin, such as the MCP Inspector at http://localhost:6274 connecting directly:
./arcane-mcp --http 127.0.0.1:8080 --http-trusted-origins http://localhost:6274A shared HTTP endpoint for a team, each member bringing their own Arcane key, reachable from one web app:
ARCANE_URL=https://arcane.example.com \ARCANE_MCP_HTTP_TOKEN=$(openssl rand -hex 32) \./arcane-mcp --http 0.0.0.0:8080 --http-max-api-keys 20 \ --http-trusted-origins https://app.example.com \ --http-allowed-hosts mcp.example.comA remote endpoint where each user connects with nothing but their Arcane API key as the Bearer token (for connectors that can only set Authorization), behind a reverse proxy on 10.0.0.2, with 5 calls per second per key:
ARCANE_URL=https://arcane.example.com \ARCANE_MCP_HTTP_AUTH=apikey \./arcane-mcp --http 0.0.0.0:8080 --http-allowed-hosts mcp.example.com \ --http-trusted-proxies 10.0.0.2 --http-trusted-proxy-header X-Forwarded-For \ --http-rate-limit-rps 5The stateless transport behind a reverse proxy, for clients that speak protocol 2026-07-28:
ARCANE_URL=https://arcane.example.com \ARCANE_MCP_HTTP_TOKEN=$(openssl rand -hex 32) \./arcane-mcp --http 0.0.0.0:8080 --http-stateless --http-allowed-hosts mcp.example.comEverything except Swarm node management and image pruning, previewing every change:
ARCANE_MCP_EXCLUDE_TOOLS=swarm_node,image.prune \ARCANE_MCP_SAFE_MODE=true \./arcane-mcp