MCP Authentication¶
The streamable-http transport requires bearer-token authentication on /mcp,
the MCP endpoint. Two routes answer without a token: GET /health (which adds
runtime details when a valid token is sent) and the OAuth metadata route
GET /.well-known/oauth-protected-resource. stdio mode has no
authentication: the client that launches the process owns it (single user,
local process only).
Static Bearer Tokens¶
The default verifier maps each token to a client identity, read from environment variables when the server starts.
Setup¶
Generate one token per MCP client:
Set MACHINA_MCP_TOKENS_JSON to a JSON object of {"<token>": "<client_id>"}:
Every token must be at least 32 characters; the server refuses to start
otherwise (the error counts the short tokens but never prints them), so a
placeholder copied from an example .env file cannot become a working
credential. The server also refuses to start on HTTP when no token source is
configured.
The client_id is attached to the verified access token of each request. It
is not yet written to traces, logs or CMMS records, so it does not give you a
per-client audit trail in v0.4.
MCP Client Configuration¶
{
"mcpServers": {
"machina": {
"url": "http://localhost:8000/mcp",
"headers": {
"Authorization": "Bearer <token-1>"
}
}
}
}
Legacy Format¶
The older MACHINA_MCP_TOKENS variable accepts comma-separated tokens
without identities (it is read only when MACHINA_MCP_TOKENS_JSON is unset,
and it emits a deprecation warning):
All of them get client_id="machina-unattributed", and the same 32-character
minimum applies.
Token Verification Flow¶
- The client sends an
Authorization: Bearer <token>header. StaticBearerTokenVerifier.verify_token()looks the token up.- Found: it returns an
AccessTokenwith theclient_idand themcp:usescope, which the server requires. - Not found: the request is rejected with
401 Unauthorized.
Every valid token can call every registered tool, writes included; there are
no per-token permissions. Use sandbox: true in the config to make the whole
server read-only toward the CMMS.
Allowed Hosts and Origins¶
The HTTP transport also checks the Host header (and the Origin header,
when present) to block DNS-rebinding attacks. The default hosts are the
loopback names localhost, 127.0.0.1 and [::1], with or without a port.
The default origins are the same loopback names over http and https, with
or without a port (http://localhost, https://127.0.0.1:8443,
http://[::1]:5173, ...). Setting either list replaces its defaults. When
clients reach the server under another name, list it in the config:
mcp:
allowed_hosts: ["mcp.plant.example", "mcp.plant.example:*"]
allowed_origins: ["https://mcp.plant.example"]
An authenticated request to /mcp whose Host is not in the list gets
421 Misdirected Request; a disallowed Origin gets 403. Behind a reverse
proxy, either forward the original Host header (and list it) or keep the
proxy's upstream host a loopback name.
Custom Token Verifiers¶
mcp.token_verifier_class replaces the static verifier with a class of your
own:
- The dotted path must start with
machina.: the server refuses any other namespace, so a config file cannot load arbitrary code. In v0.4 the package ships only the static verifier, so a custom one means adding a module inside themachinapackage (for example in a fork or a vendored build). - Naming the built-in static verifier here is refused: leave
token_verifier_classempty and setMACHINA_MCP_TOKENS_JSONinstead. - The class is constructed with the whole loaded configuration,
cls(config). - It implements
async def verify_token(self, token: str) -> AccessToken | None(the MCP SDK'sTokenVerifierprotocol) and must grant themcp:usescope.
from mcp.server.auth.provider import AccessToken
class VaultTokenVerifier:
"""Verify tokens against HashiCorp Vault (illustrative)."""
def __init__(self, config):
self._vault = make_vault_client(config) # your client
async def verify_token(self, token: str) -> AccessToken | None:
identity = await self._vault.lookup(token)
if identity is None:
return None
return AccessToken(token=token, client_id=identity["name"], scopes=["mcp:use"])
Security Considerations¶
- Rotate tokens immediately if one is compromised; restart the server to pick up the new set.
- Bind to localhost (the default,
--host 127.0.0.1) unless the server sits behind a TLS-terminating reverse proxy. Machina does not terminate TLS, and bearer tokens over plain HTTP are readable on the network. - One token per client, so you can revoke a single client without rotating the others.
- See Security for the full threat model.