This guide covers configuration precedence, environment variables, auth strategy, Docker, HTTP mode, and least-privilege deployment patterns.
The server resolves configuration in this order:
- Environment variables
- Saved config file at
$XDG_CONFIG_HOME/affine-mcp/configwhenXDG_CONFIG_HOMEis set, otherwise~/.config/affine-mcp/config - Built-in defaults
The saved config file uses the same KEY=value names shown below. Environment variables always override saved values, and the CLI diagnostics report the source selected for each runtime option.
Authentication credentials are resolved as one source-scoped group. If the
environment provides any of AFFINE_API_TOKEN, AFFINE_COOKIE,
AFFINE_EMAIL, or AFFINE_PASSWORD, saved authentication credentials are not
mixed into that environment configuration. An Authorization or Cookie
entry in an environment-provided AFFINE_HEADERS_JSON also selects the
environment authentication group. Non-authentication headers from saved
configuration remain available when no environment AFFINE_HEADERS_JSON
replaces them.
Auth priority within the active configuration:
AFFINE_API_TOKENAFFINE_COOKIEAFFINE_EMAILandAFFINE_PASSWORD
This priority is applied only within the selected environment or saved-config group. For example, environment email/password credentials take precedence over an older saved API token or session cookie.
Email/password authentication is process-scoped. Concurrent HTTP MCP sessions
share one sign-in attempt and the resulting cookie. In the default async
mode, transport startup is not blocked, but the first backend operation waits
for authentication to finish. A failed login is returned to every waiting
operation and is never retried as an unauthenticated request.
Bearer and cookie credentials are mutually exclusive on outbound requests.
Explicit sign_in replaces the current client credential with its session
cookie, while setting a bearer credential removes any cookie header.
| Variable | Required | Default | Notes |
|---|---|---|---|
AFFINE_BASE_URL |
No | http://localhost:3010 |
Base URL for AFFiNE Cloud or self-hosted AFFiNE; remote destinations must use HTTPS |
AFFINE_ALLOW_INSECURE_HTTP |
No | false |
Explicitly allow a remote plain-HTTP AFFiNE URL on a trusted private network only |
AFFINE_GRAPHQL_PATH |
No | /graphql |
Override only if your AFFiNE deployment uses a custom GraphQL path |
AFFINE_HEADERS_JSON |
No | none | JSON object of additional string headers sent to AFFiNE; built-in token/cookie auth takes priority |
AFFINE_WORKSPACE_ID |
No | Auto-detected when possible | Pins the active workspace |
AFFINE_LOGIN_AT_START |
No | async |
async starts one shared login without blocking transport startup; sync requires login before startup |
AFFINE_CLIENT_VERSION |
No | 0.26.0 |
AFFiNE web-client version sent as the x-affine-version header on GraphQL/REST requests. Servers that gate on client version reject sign-in with 403 UNSUPPORTED_CLIENT_VERSION when it is too low; raise this if your deployment pins a higher minimum. Also used as the fallback default for AFFINE_WS_CLIENT_VERSION |
XDG_CONFIG_HOME |
No | ~/.config |
Changes the parent directory used for the saved affine-mcp/config file |
| Variable | Required | Default | Notes |
|---|---|---|---|
AFFINE_BLOB_UPLOAD_MAX_BYTES |
No | 26214400 (25 MiB) |
Maximum decoded payload size accepted by upload_blob |
AFFINE_BLOB_UPLOAD_TIMEOUT_MS |
No | 30000 |
Maximum time allowed for the multipart upload request |
AFFINE_BLOB_UPLOAD_RESPONSE_MAX_BYTES |
No | 1048576 (1 MiB) |
Maximum AFFiNE response body size read after an upload |
upload_blob treats content as UTF-8 by default and preserves it exactly, including leading and trailing whitespace. Binary callers must pass encoding: "base64"; Base64 input is validated for canonical padding before it is decoded. ASCII whitespace inside explicit Base64 input is ignored.
| Variable | Use when | Notes |
|---|---|---|
AFFINE_API_TOKEN |
A legacy or compatible AFFiNE deployment accepts GraphQL bearer tokens | AFFiNE 0.27+ removed the legacy personal-access-token API; this server cannot generate one |
AFFINE_COOKIE |
Reuse browser-authenticated state, including AFFiNE Cloud | Copy the complete Cookie request header only from a trusted local browser session |
AFFINE_EMAIL |
Self-hosted email/password sign-in | Recommended for current self-hosted AFFiNE; must be paired with AFFINE_PASSWORD |
AFFINE_PASSWORD |
Self-hosted email/password sign-in | Use a dedicated least-privilege account for unattended deployments |
| Variable | Purpose |
|---|---|
AFFINE_TOOL_PROFILE |
Environment-only predefined tool surface profile (full, read_only, core, authoring) |
AFFINE_DISABLED_GROUPS |
Environment-only comma-separated tool groups to disable |
AFFINE_DISABLED_TOOLS |
Environment-only exact canonical tool names to disable |
| Variable | Required | Default | Notes |
|---|---|---|---|
MCP_TRANSPORT |
No | stdio |
Set to http; streamable and legacy sse are accepted aliases |
PORT |
No | 3000 |
Commonly injected by container platforms |
AFFINE_MCP_AUTH_MODE |
No | bearer |
bearer or oauth |
AFFINE_MCP_HTTP_HOST |
No | 127.0.0.1 |
Use 0.0.0.0 in containers; non-loopback bearer listeners require authentication |
AFFINE_MCP_HTTP_ALLOWED_ORIGINS |
No | none | Comma-separated list for browser clients |
AFFINE_MCP_HTTP_ALLOW_ALL_ORIGINS |
No | false |
Testing only; rejected in OAuth mode |
AFFINE_MCP_HTTP_TOKEN |
Required for non-loopback bearer mode | none | Shared bearer token for /mcp, /sse, and /messages |
AFFINE_MCP_HTTP_ALLOW_UNAUTHENTICATED |
No | false |
Unsafe opt-in for an unauthenticated non-loopback bearer-mode listener |
AFFINE_MCP_HTTP_ALLOW_QUERY_TOKEN |
No | false |
Deprecated compatibility mode for ?token= clients; prefer the Authorization header |
AFFINE_MCP_HTTP_BODY_LIMIT |
No | 4mb |
Maximum JSON request body size; accepts bytes, kb, or mb from 1kb through 64mb |
AFFINE_MCP_HTTP_MAX_SESSIONS |
No | 32 |
Maximum combined Streamable HTTP and legacy SSE sessions |
AFFINE_MCP_HTTP_SESSION_IDLE_TIMEOUT_MS |
No | 1800000 |
Close sessions that receive no MCP activity for this duration |
AFFINE_MCP_HTTP_SHUTDOWN_TIMEOUT_MS |
No | 10000 |
Deadline before remaining HTTP connections are forcibly closed |
AFFINE_MCP_PUBLIC_BASE_URL |
Required in OAuth mode | none | Public base URL for this MCP server |
AFFINE_OAUTH_ISSUER_URL |
Required in OAuth mode | none | OAuth issuer discovery URL |
AFFINE_OAUTH_SCOPES |
No | mcp |
Scopes advertised for OAuth-protected access |
AFFINE_OAUTH_CLOCK_SKEW_SECONDS |
No | 60 |
Positive integer tolerance for OAuth token timestamps |
AFFINE_OAUTH_ALLOW_SERVICE_WRITES |
No | false |
Explicitly acknowledge write-capable tools using the shared AFFiNE service identity |
| Variable | Required | Default | Notes |
|---|---|---|---|
AFFINE_WS_CLIENT_VERSION |
No | AFFINE_CLIENT_VERSION (else 0.26.0) |
Environment-only AFFiNE client version sent during workspace socket connection; falls back to AFFINE_CLIENT_VERSION when unset |
AFFINE_WS_CONNECT_TIMEOUT_MS |
No | 10000 |
Environment-only milliseconds to wait for a workspace socket connection |
AFFINE_WS_ACK_TIMEOUT_MS |
No | 10000 |
Environment-only milliseconds to wait for a workspace socket acknowledgement |
| Environment | Recommended auth | Why |
|---|---|---|
| AFFiNE Cloud + stdio | Browser session cookie or saved config from affine-mcp login |
Cloud programmatic sign-in is blocked by Cloudflare |
| AFFiNE Cloud + HTTP | Browser session cookie plus bearer or OAuth at the MCP layer | MCP caller auth and AFFiNE backend auth remain separate |
| Self-hosted + stdio | Email/password or a session cookie | Supported by current AFFiNE without the removed token API |
| Self-hosted + HTTP | Dedicated-account email/password or a session cookie | Suitable for shared backend service identity deployments |
Important note for AFFiNE Cloud:
- Programmatic email/password sign-in to
/api/auth/sign-inis not supported because Cloudflare blocks those requests - AFFiNE's built-in MCP credentials authenticate its own workspace-scoped MCP endpoint and are not interchangeable with this external server's GraphQL credentials
Prebuilt images are published to GHCR:
ghcr.io/dawncr0w/affine-mcp-server:latestghcr.io/dawncr0w/affine-mcp-server:3.0.0
Example:
docker run -d \
-p 3000:3000 \
-e MCP_TRANSPORT=http \
-e AFFINE_BASE_URL=https://your-affine-instance.com \
-e AFFINE_EMAIL=you@example.com \
-e AFFINE_PASSWORD=your-password \
-e AFFINE_MCP_AUTH_MODE=bearer \
-e AFFINE_MCP_HTTP_TOKEN=your-strong-secret \
ghcr.io/dawncr0w/affine-mcp-server:latestHealth endpoints:
/healthz/readyz
HTTP mode exposes:
/mcp- Streamable HTTP MCP endpoint protected by the configured MCP auth mode/sse- SSE endpoint for older-compatible clients protected by the configured MCP auth mode/messages- message endpoint for older-compatible clients protected by the configured MCP auth mode/healthz- unauthenticated process liveness probe for trusted platform checks/readyz- unauthenticated readiness probe that checks OAuth discovery when enabled and the exact configured AFFiNE GraphQL endpoint
/readyz returns 503 with the failing component when the configured AFFiNE GraphQL endpoint is unavailable. Keep both diagnostic routes private to your load balancer or trusted monitoring network.
The HTTP transport limits JSON request bodies and the number of active sessions
to prevent accidental resource exhaustion. Both Streamable HTTP and legacy SSE
sessions count toward AFFINE_MCP_HTTP_MAX_SESSIONS. New sessions receive a
503 response with Retry-After when the limit is reached. Existing session
traffic refreshes its idle deadline, and inactive sessions are closed after
AFFINE_MCP_HTTP_SESSION_IDLE_TIMEOUT_MS.
On SIGINT or SIGTERM, the server stops accepting connections and closes MCP
transports concurrently. If a connection prevents graceful shutdown beyond
AFFINE_MCP_HTTP_SHUTDOWN_TIMEOUT_MS, the remaining HTTP connections are
forcibly closed. Invalid runtime limit values and listen errors fail startup
instead of leaving a partially running process.
export MCP_TRANSPORT=http
export AFFINE_MCP_AUTH_MODE=bearer
export AFFINE_BASE_URL="https://your-self-hosted-affine.example.com"
export AFFINE_EMAIL="service-account@example.com"
export AFFINE_PASSWORD="your-service-account-password"
export AFFINE_MCP_HTTP_HOST="0.0.0.0"
export AFFINE_MCP_HTTP_TOKEN="your-super-secret-token"
export PORT=3000
npm run start:httpUse bearer mode when:
- the client can inject a shared secret header
- you want the simplest remote deployment
- you do not need OAuth discovery and token validation
Bearer tokens must be sent with Authorization: Bearer <token>. Query-string
tokens are rejected by default because URLs can be retained in access logs,
browser history, and monitoring systems. Legacy clients can temporarily opt in
with AFFINE_MCP_HTTP_ALLOW_QUERY_TOKEN=true, but this mode is deprecated.
In bearer mode, a non-loopback listener such as 0.0.0.0, ::, a LAN address,
or a hostname requires AFFINE_MCP_HTTP_TOKEN. Startup fails when the token is
missing. AFFINE_MCP_HTTP_ALLOW_UNAUTHENTICATED=true is an explicit unsafe
escape hatch for isolated private networks and must not be used on an
internet-reachable listener. Loopback listeners remain available without MCP
authentication for local development.
export MCP_TRANSPORT=http
export AFFINE_MCP_AUTH_MODE=oauth
export AFFINE_BASE_URL="https://your-self-hosted-affine.example.com"
export AFFINE_EMAIL="service-account@example.com"
export AFFINE_PASSWORD="your-service-account-password"
export AFFINE_MCP_HTTP_HOST="0.0.0.0"
export AFFINE_MCP_PUBLIC_BASE_URL="https://mcp.yourdomain.com"
export AFFINE_OAUTH_ISSUER_URL="https://auth.yourdomain.com"
export AFFINE_OAUTH_SCOPES="mcp"
export PORT=3000
npm run start:httpOAuth mode behavior:
- exposes
/.well-known/oauth-protected-resource - returns
401+WWW-Authenticatechallenge for unauthenticated/mcprequests - disables
AFFINE_MCP_HTTP_TOKENand?token= - does not register
sign_in - still requires backend service credentials so the server can call AFFiNE: email/password, a session cookie, or a compatible bearer token
- authenticates callers at the MCP boundary but does not delegate their identity to AFFiNE; every request uses the same configured backend service identity
- defaults
AFFINE_TOOL_PROFILEtoread_onlywhen no profile is configured - refuses any write-capable tool surface unless
AFFINE_OAUTH_ALLOW_SERVICE_WRITES=trueis also set
To allow service-account writes, configure both controls explicitly:
export AFFINE_TOOL_PROFILE="authoring"
export AFFINE_OAUTH_ALLOW_SERVICE_WRITES="true"This grants every OAuth caller accepted by the configured issuer the same AFFiNE mutation permissions. Use separate deployments or backend credentials when callers require different AFFiNE authorization boundaries.
Profiles are the easiest way to reduce the MCP tool surface without listing every tool by name.
Example:
{
"AFFINE_TOOL_PROFILE": "core"
}Available profiles:
full: expose the complete public tool surface; this is the default outside OAuth moderead_only: expose discovery, reading, export, fidelity, and inspection tools, plussign_incore: expose the compact everyday surface for workspace/doc discovery, basic document authoring, tags, and database row/schema edits; omits admin tools, cleanup tools, experimental organize tools, and destructive toolsauthoring: expose non-destructive creation and editing tools, including semantic pages, native templates, database composition, and edgeless canvas authoring; omits admin, cleanup, destructive, and experimental organize tools
Profile, group, and tool names are validated at startup. An unknown value stops the server instead of falling back to a broader tool surface. This prevents a configuration typo from silently enabling tools that an operator intended to hide.
Example:
{
"AFFINE_DISABLED_GROUPS": "comments,history,blobs,users"
}Current group names:
workspacesworkspaces.readworkspaces.writedocsdocs.readdocs.writedocs.markdowndocs.tagsdocs.treedocs.exportdocs.semanticdocs.templatedocs.databasedocs.edgelessdocs.surfacedocs.intentdocs.sharecommentscomments.readcomments.writehistoryhistory.readorganizeorganize.readorganize.writeorganize.collectionsorganize.foldersusersusers.readusers.writeusers.authblobsblobs.writenotificationsnotifications.readnotifications.writeadminauthcleanupdestructiveexperimentalreadwrite
Example:
{
"AFFINE_DISABLED_TOOLS": "delete_workspace,delete_doc"
}Use tool-level filtering when you want a mostly complete tool surface but need to remove specific destructive or administrative operations.
Every registered tool must also be present in the canonical tool surface and tool-manifest.json. The server refuses to start when a tool is registered without that metadata, including when the full profile is selected.
Before exposing the server remotely, confirm:
AFFINE_BASE_URLis reachable from the MCP host- the configured AFFiNE backend credentials work through
affine-mcp statusor an equivalent health path MCP_TRANSPORT=httpis setAFFINE_MCP_AUTH_MODEis correct for your client modelAFFINE_MCP_HTTP_HOST=0.0.0.0is set in containerized deployments- HTTPS or TLS termination is in front of any non-local HTTP deployment
- bearer mode uses a long random
AFFINE_MCP_HTTP_TOKEN, or OAuth is configured for multi-user access - clients send bearer credentials in the
Authorizationheader rather than the URL AFFINE_MCP_HTTP_ALLOW_UNAUTHENTICATEDandAFFINE_MCP_HTTP_ALLOW_QUERY_TOKENare not enabledAFFINE_MCP_HTTP_ALLOWED_ORIGINSis set for browser-based clientsAFFINE_MCP_HTTP_ALLOW_ALL_ORIGINSis not enabled outside local testing/healthzand/readyzare wired into your platform checks- destructive tools are filtered if your deployment should be read-only or constrained
- Cloudflare / sign-in failures: use a signed-in browser session cookie
- Startup timeouts: avoid
AFFINE_LOGIN_AT_START=syncunless required - Missing tools: confirm filtering variables are not removing them
- Browser CORS failures: verify
AFFINE_MCP_HTTP_ALLOWED_ORIGINS - OAuth failures: verify issuer discovery metadata and JWKS availability
- Custom GraphQL deployments: run
affine-mcp show-config --jsonand confirmgraphqlEndpoint, then runaffine-mcp doctor --json doctoralso rejects an unprotected non-loopback HTTP bind and validates OAuth transport, discovery metadata, and JWKS reachability- Invalid transport, port, origin, or boolean values now fail at startup instead of silently falling back
- Remote plain-HTTP AFFiNE URL rejected: use HTTPS, or set
AFFINE_ALLOW_INSECURE_HTTP=trueonly for a trusted private network - Non-loopback bearer listener rejected: set
AFFINE_MCP_HTTP_TOKENor configure OAuth