earl

Policy Engine

JWT-based access control for multi-user and multi-agent MCP HTTP deployments.

The policy engine controls which MCP callers can invoke which tools. It only comes into play when Earl's HTTP transport is running with JWT authentication — if you're using stdio, or you passed --allow-unauthenticated, none of this applies.

JWT Authentication

Configure JWT validation in ~/.config/earl/config.toml.

If your identity provider publishes a standard discovery document, use this. Earl fetches the JWKS endpoint automatically.

[auth.jwt]
audience = "https://api.yourcompany.com"
oidc_discovery_url = "https://accounts.yourcompany.com/.well-known/openid-configuration"

Manual JWKS

Use this when there's no discovery endpoint.

[auth.jwt]
audience   = "https://api.yourcompany.com"
issuer     = "https://accounts.yourcompany.com"
jwks_uri   = "https://accounts.yourcompany.com/.well-known/jwks.json"
algorithms = ["RS256"]
clock_skew_seconds         = 30
jwks_cache_max_age_seconds = 900

Fields

FieldTypeRequiredDefaultDescription
audiencestringYesExpected aud claim
oidc_discovery_urlstringIf issuer/jwks_uri not setDiscovery URL; resolves issuer and JWKS URI automatically
issuerstringIf oidc_discovery_url not setExpected iss claim
jwks_uristringIf oidc_discovery_url not setJWKS endpoint
algorithmsstring[]No["RS256"]Allowed signing algorithms
clock_skew_secondsintegerNo30Clock skew tolerance (max 300)
jwks_cache_max_age_secondsintegerNo900JWKS cache TTL in seconds

Supported algorithms: RS256, RS384, RS512, ES256, ES384, ES512, PS256, PS384, PS512, EdDSA.

Incoming tokens must carry exp, sub, iss, and aud claims. The sub claim is what policy rules match against.

Policy Rules

Add [[policy]] entries to ~/.config/earl/config.toml. Each rule targets a set of subjects, a set of tools, an optional mode filter, and an effect.

# All authenticated users can read from GitHub
[[policy]]
subjects = ["user:*"]
tools    = ["github.*"]
modes    = ["read"]
effect   = "allow"

# Admins can do anything with GitHub tools
[[policy]]
subjects = ["group:admins"]
tools    = ["github.*"]
effect   = "allow"

# Nobody can invoke write-mode destructive commands
[[policy]]
subjects = ["*"]
tools    = ["github.delete_repo", "github.merge_pull"]
modes    = ["write"]
effect   = "deny"

Fields

FieldTypeRequiredDescription
subjectsstring[]YesCaller identity patterns
toolsstring[]YesTool name patterns
modesstring[]No"read", "write", or both. Omit to match any mode.
effectstringYes"allow" or "deny"

Subject patterns

PatternMatches
user:aliceJWT sub equals alice
user:*Any sub value
group:adminsCallers with admins in their groups claim
*Any authenticated caller

Tool patterns

* matches within a single dot-separated segment.

PatternMatchesDoes not match
github.*github.create_issue, github.search_reposslack.send_message
*.delete_*github.delete_repo, stripe.delete_customergithub.admin.delete
*Any tool

How evaluation works

Earl uses deny-overrides: collect every rule that matches the subject, tool, and mode. If any of them is a deny, the call is rejected. If there's at least one allow and no deny, the call goes through. If nothing matches, the call is rejected — there's no implicit allow.

One deny beats any number of allows.

Multi-team example

# Team Alpha can read from GitHub and Stripe
[[policy]]
subjects = ["group:alpha"]
tools    = ["github.*", "stripe.*"]
modes    = ["read"]
effect   = "allow"

# Team Beta has full GitHub access
[[policy]]
subjects = ["group:beta"]
tools    = ["github.*"]
effect   = "allow"

# Nobody runs write-mode system commands over MCP
[[policy]]
subjects = ["*"]
tools    = ["system.*"]
modes    = ["write"]
effect   = "deny"

Starting the HTTP server

earl mcp http --listen 0.0.0.0:8977

Earl won't start without either [auth.jwt] configured or --allow-unauthenticated passed. You can't use both.

For local dev:

earl mcp http --listen 127.0.0.1:8977 --allow-unauthenticated

--allow-unauthenticated skips authentication and the policy engine. Don't use it anywhere shared.

See also

On this page