Skip to content

Configuration

arcane-mcp reads its configuration from three places. In increasing order of precedence:

  1. a .env file in the working directory (KEY=value per line; # comments and an export prefix are accepted; it never overrides a variable already set),
  2. the process environment,
  3. 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.

VariableFlagDefaultMeaning
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--environment0Environment 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-verifyfalseAccept self-signed certificates
ARCANE_MCP_TOOL_SURFACE--tool-surfacedynamicdynamic, meta or individual
ARCANE_MCP_READ_ONLY--read-onlyfalseExpose only read-only actions
ARCANE_MCP_SAFE_MODE--safe-modefalseAnswer 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—4194304Cap for downloads and collected stream output
ARCANE_MCP_STDIO_MAX_LINE_BYTES—16777216Largest 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—60sTimeout of one non-streaming request
ARCANE_MCP_ACTION_TIMEOUT--action-timeout30m, or ARCANE_MCP_TIMEOUT when that is longerOverall 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--elicitationtrueAsk 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-timeout5mHow long to wait for the user’s answer before refusing
ARCANE_MCP_AUDIT_LOG--audit-logtrueOne info log record per tool call (see Safety)
ARCANE_MCP_AUDIT_LOG_ARG_NAMES--audit-log-arg-namesfalseAdd 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-keys100Distinct 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-timeout10mReclaim 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-timeout30mClose a session after this long without a request (0: never)
ARCANE_MCP_HTTP_MAX_SESSIONS--http-max-sessions1000Live sessions over every key
ARCANE_MCP_HTTP_MAX_CONCURRENT_REQUESTS--http-max-concurrent-requests64POST and DELETE requests served at once over every client (event streams are not counted)
ARCANE_MCP_HTTP_MAX_BODY_BYTES--http-max-body-bytes16777216Largest 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-statelessfalseServe 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-authtokenHow 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-ttl5mapikey mode: how long a key Arcane accepted is served before Arcane is asked again
ARCANE_MCP_HTTP_AUTH_MAX_FAILURES--http-auth-max-failures10Failed 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-window1mThe window of ARCANE_MCP_HTTP_AUTH_MAX_FAILURES, counted from the first failure
ARCANE_MCP_HTTP_AUTH_NETWORK_FACTOR--http-auth-network-factor10An 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-rps0 (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-mode0660Permission (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-delay0sAfter 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-burst40Requests one key may make at once before the rate paces it (at least 1 when the rate is set)
ARCANE_MCP_HTTP_SERVER_CARD—trueServe the server card at /server-card and /mcp/server-card
ARCANE_MCP_HTTP_DISCOVERY_RATE_LIMIT_RPS--http-discovery-rate-limit-rps20Requests 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-burst200Server card requests one client address may make at once (0: no limit; at most 10000)
ARCANE_MCP_ICONS—trueSend 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—falseExperimental: on the individual surface, declare each action’s output schema, derived from Arcane’s OpenAPI document (see Output schemas)
ARCANE_MCP_LOG_LEVEL--log-levelinfodebug, 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 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 .env file can fill it. Desktop extensions and registry clients send an empty value for a field the user cleared, so an empty ARCANE_MCP_TOOL_SURFACE is the default surface and an empty ARCANE_ENVIRONMENT_ID is environment 0.
  • 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_TIMEOUT must 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: true ask 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, or ARCANE_MCP_TIMEOUT when 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_LEVEL is read when ARCANE_MCP_LOG_LEVEL is unset, with a deprecation warning; a LOG_LEVEL value that is not a level is ignored with a warning. An unreadable ARCANE_MCP_LOG_LEVEL or --log-level stops the server at startup.
  • An ARCANE_MCP_EXCLUDE_TOOLS entry 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 429 with Retry-After until the minute ends (ARCANE_MCP_HTTP_AUTH_MAX_FAILURES, 0 turns it off); behind a reverse proxy set ARCANE_MCP_HTTP_TRUSTED_PROXIES and ARCANE_MCP_HTTP_TRUSTED_PROXY_HEADER, or every client shares the proxy’s budget. ARCANE_MCP_HTTP_AUTH=apikey and a per-key rate limit are off by default (see HTTP authentication).
  • The auth-failure budget also blocks an IPv6 /48 as a whole once it has had ten times ARCANE_MCP_HTTP_AUTH_MAX_FAILURES credentials refused within one window (see HTTP authentication). The factor is ARCANE_MCP_HTTP_AUTH_NETWORK_FACTOR (10; 0 turns the /48 limit off).
  • Two trusted-proxy settings that cannot work stop the server at startup: a unix-socket ARCANE_MCP_HTTP_ADDR whose ARCANE_MCP_HTTP_TRUSTED_PROXIES lists addresses but not unix (a socket peer has no address, so the header would never be believed), and unix in 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_ADDR that 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 data are 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=json does 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 shows versionMatch and catalogVersion; 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.ParseBool does (1, true, TRUE, 0, false…) plus yes/no and on/off. Numbers are whole numbers of bytes or items (16777216, not 16MiB). 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 bare 60 is refused).
  • ARCANE_ENVIRONMENT_ID is a default, not a restriction. Every action whose Arcane route lives under /environments/{id}/ takes an optional environmentId parameter; omitted, it is this value. Use environment.list to find the IDs.
  • ARCANE_MCP_EXCLUDE_TOOLS matches 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_TIMEOUT bounds ordinary requests. Progress streams (image pull and build, project up, pull, build and redeploy) are read to the end, within ARCANE_MCP_ACTION_TIMEOUT, which bounds every action as a whole; the live-stream actions have their own seconds parameter; 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 (-32700 or -32600) and the session goes on. Closing stdin ends the process with status 0 once the requests already sent are answered, so echo '<request>' | arcane-mcp and scripts that close their end after writing get their answers; the wait is bounded by ARCANE_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 than ARCANE_MCP_HTTP_ADDR (the token, limits, TLS, socket, proxy and auth-budget settings). When any of them is set, in the environment, a .env file 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 (or ARCANE_MCP_HTTP_ADDR) serves Streamable HTTP at /mcp, plus two probes that never ask for the token:

PathAnswers
/healthzLiveness: 200 ok while the process serves, whatever the state of Arcane
/readyzReadiness: 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-Key gets its own MCP server. At most ARCANE_MCP_HTTP_MAX_API_KEYS are kept; a key’s server is reclaimed only when it has no live session and no request in flight (after ARCANE_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 gets 503 with Retry-After and 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-Key than the one it was opened with (or none, when it was opened with one) is refused with 403; 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_SESSIONS sessions live at once, a session idle for ARCANE_MCP_HTTP_SESSION_IDLE_TIMEOUT is closed (its next request gets 404, the signal to initialize again), at most ARCANE_MCP_HTTP_MAX_CONCURRENT_REQUESTS calls run at once, and a body over ARCANE_MCP_HTTP_MAX_BODY_BYTES gets 413. A body with a message nesting JSON more than 100 levels deep (a batch’s own array is not counted) gets 400 with a JSON-RPC error -40000, the request’s id when 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 with 503 and Retry-After, never left waiting.
  • Allowed hosts. With ARCANE_MCP_HTTP_ALLOWED_HOSTS set, /mcp answers only a request whose Host is listed (403 otherwise), 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. /healthz and /readyz are not restricted.
  • Cross-origin protection. A request a browser makes on behalf of another site (by Sec-Fetch-Site, or an Origin naming another host) is refused with 403 on every method, unless its origin is in ARCANE_MCP_HTTP_TRUSTED_ORIGINS, which also gets the CORS answers a browser needs. Clients that send no Origin (every non-browser MCP client) are not affected. The SDK also refuses a request that reaches a loopback address with a non-loopback Host (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 null id. Later refusals read at most 4 KiB of the body, for at most a second, to answer with the request’s id. 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 id when it was read: the standard -32700, -32600 and -32601 when the message itself is at fault, otherwise the HTTP status times −100 (-40100 for a missing token, -40300, -42900, -50300…). The 404 for an unknown session stays plain, as clients expect.

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-URL header: a listed name (home) or exactly a listed URL (a trailing slash does not matter). A missing header is refused with 400, an unlisted value with 403; 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). In apikey mode, 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 (403 when used with another). In apikey mode the key is checked with GET /api/auth/me on 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_KEY must then be a name=key list (a single key would be sent to every instance); an instance without a key serves only clients that bring their own. ARCANE_USERNAME/ARCANE_PASSWORD serve one instance only, so with several over HTTP they stop the server at startup. ARCANE_ENVIRONMENT_ID may be one value for all or a name=id list.
  • 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.
  • /readyz checks every instance and lists them by name (never by URL): 200 ready when all answer, 200 degraded when some do (the server still serves the others’ clients, and a 503 would take it out of a load balancer for all of them), 503 when none does. Each failed check is logged. The JSON form (?format=json) lists each instance’s name, status and versionMatch in instances. /readyz asks 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.

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): with ARCANE_MCP_HTTP_TOKEN every request needs Authorization: Bearer <token>; each client brings its Arcane key in X-Arcane-API-Key or runs as the server’s ARCANE_API_KEY or login. A client that sends the server’s own ARCANE_API_KEY is 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 set Authorization (many remote connectors, mcp-remote) needs nothing else. The first request with a key asks Arcane GET /api/auth/me with it (a read every user may make of itself; Arcane only notes the key’s last use). The key is accepted only when Arcane answers 200 with its user; it is then served for ARCANE_MCP_HTTP_AUTH_CACHE_TTL without asking again, and forgotten at once when a call with it gets 401 from Arcane (a key revoked meanwhile). A key Arcane refuses (401, or an environment access token, which Arcane accepts in X-API-Key but which is no user’s key) gets 401 with WWW-Authenticate: Bearer error="invalid_token". Any other answer (Arcane unreachable, a 403 or a redirect from something in front of it, a page that is not Arcane’s) gets 503 with Retry-After, and nothing is remembered. There is no fallback to a server credential, so ARCANE_MCP_HTTP_TOKEN, ARCANE_API_KEY and the login are refused at startup in this mode. X-Arcane-API-Key is not needed; if a client sends it anyway it must be the same key as the Bearer (400 otherwise). Sessions are bound to the Bearer key, as they are to X-Arcane-API-Key in 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.

  • TLS. With ARCANE_MCP_HTTP_TLS_CERT and ARCANE_MCP_HTTP_TLS_KEY the 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 :8080 or 0.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=false is 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_ADDR is a path (it contains /, or \ on Windows) or unix:<path>, the server listens on a unix socket with ARCANE_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_HOSTS and 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, unless ARCANE_MCP_HTTP_TRUSTED_PROXIES contains unix (a reverse proxy on the same host) and ARCANE_MCP_HTTP_TRUSTED_PROXY_HEADER names the client. Windows 10 and later have unix sockets too (C:\ProgramData\arcane-mcp\mcp.sock, or unix: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_MODE does not decide who can connect there.
  • arcane-mcp --probe asks the local listener for /healthz (/readyz with --ready) and exits 0 on a 200, 1 otherwise, within --timeout (3 s). It reads ARCANE_MCP_HTTP_ADDR and ARCANE_MCP_HTTP_TLS_CERT the way the server does (a .env file, the environment, or --http / --http-tls-cert after --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 port 0 cannot be found: pass the logged port with --http. Without an HTTP address (stdio mode) it says so and exits 0. The container image’s HEALTHCHECK runs it: an HTTP container is healthy once /healthz answers, 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 (or Accept: application/json alone; an MCP client’s application/json, text/event-stream keeps 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. Plain ok stays the default.
  • Draining. With ARCANE_MCP_HTTP_DRAIN_DELAY, SIGTERM first turns /readyz to 503 (and draining to true) 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).

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, repository and icons;
  • one Streamable HTTP endpoint, {baseUrl}/mcp. baseUrl is 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) when ARCANE_MCP_HTTP_TOKEN is set or in apikey mode; X-Arcane-API-Key in 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 in apikey mode, 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).

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 revisions2025-11-25 and earlier; a 2026-07-28 client is told to fall back2026-07-28 and earlier
SessionsMcp-Session-Id, bound to the key that opened it (403 for another key)None: an Mcp-Session-Id sent is ignored, never refused
Arcane keyThe one the session was opened withRead from every POST (X-Arcane-API-Key, else the server’s key or login), so consecutive requests may use different keys
GET (event stream), DELETEServed405 with Allow: POST and a JSON-RPC error (-40500)
Confirmation of destructive callsElicitation on every revisionOn 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 notificationsOn the call’s own event streamSame: they travel on the POST’s event stream while the call runs
ARCANE_MCP_HTTP_MAX_SESSIONS, ARCANE_MCP_HTTP_SESSION_IDLE_TIMEOUTApplyDo not apply
A key’s server counts as in useWhile it has a live session or a request in flightOnly 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.

A read-only assistant on the meta surface, pinned to one remote environment:

Terminal window
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:

Terminal window
./arcane-mcp --http 127.0.0.1:8080 --http-trusted-origins http://localhost:6274

A shared HTTP endpoint for a team, each member bringing their own Arcane key, reachable from one web app:

Terminal window
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.com

A 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:

Terminal window
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 5

The stateless transport behind a reverse proxy, for clients that speak protocol 2026-07-28:

Terminal window
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.com

Everything except Swarm node management and image pruning, previewing every change:

Terminal window
ARCANE_MCP_EXCLUDE_TOOLS=swarm_node,image.prune \
ARCANE_MCP_SAFE_MODE=true \
./arcane-mcp