OAuth for MCP clients
Lantfly is its own OAuth 2.1 authorization server for the MCP server. An MCP client that supports the
MCP authorization spec needs only the
address https://lantfly.com/mcp: it discovers the rest, and you approve it on a Lantfly page. Nobody copies a token.
You don't need this page to connect an app: Connect clients covers that. Read on if you build an MCP client or want to know what happens.
The flow at a glance
- The client calls
POST https://lantfly.com/mcpwithout a token and gets401with aWWW-Authenticateheader. - It reads the protected resource metadata, then the authorization server metadata.
- It registers itself (dynamic client registration) and gets a
client_id. - It opens
/api/oauth/authorizein your browser with a PKCE challenge. Lantfly sends you to the consent page. - You sign in, pick the account, optionally tick read-only, and click Allow.
- The client exchanges the code at
/api/oauth/tokenfor an access token (1 hour) and a refresh token. - It calls the MCP server with
Authorization: Bearer <access token>and refreshes before the hour is up.
1. Discovery
A request without a valid token answers:
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://lantfly.com/.well-known/oauth-protected-resource/mcp", scope="mcp"
Content-Type: application/json
{"error":{"code":"unauthenticated","message":"Authentication token was not provided or is invalid."}}
Protected resource metadata (RFC 9728):
curl https://lantfly.com/.well-known/oauth-protected-resource/mcp
{
"resource": "https://lantfly.com/mcp",
"authorization_servers": ["https://lantfly.com"],
"scopes_supported": ["mcp"],
"bearer_methods_supported": ["header"],
"resource_name": "Lantfly"
}
Authorization server metadata (RFC 8414), also served at
/.well-known/openid-configuration for clients that only look there:
curl https://lantfly.com/.well-known/oauth-authorization-server
{
"issuer": "https://lantfly.com",
"authorization_endpoint": "https://lantfly.com/api/oauth/authorize",
"token_endpoint": "https://lantfly.com/api/oauth/token",
"registration_endpoint": "https://lantfly.com/api/oauth/register",
"revocation_endpoint": "https://lantfly.com/api/oauth/revoke",
"response_types_supported": ["code"],
"response_modes_supported": ["query"],
"grant_types_supported": ["authorization_code", "refresh_token"],
"code_challenge_methods_supported": ["S256"],
"token_endpoint_auth_methods_supported": ["none", "client_secret_post", "client_secret_basic"],
"revocation_endpoint_auth_methods_supported": ["none", "client_secret_post", "client_secret_basic"],
"scopes_supported": ["mcp"],
"authorization_response_iss_parameter_supported": true
}
A client that was set up with the older address https://lantfly.com/api/v3/mcp gets metadata for that resource
instead (/.well-known/oauth-protected-resource/api/v3/mcp).
2. Dynamic client registration
POST /api/oauth/register (RFC 7591) is open to any client, up to 10 a minute
per address:
curl -X POST https://lantfly.com/api/oauth/register \
-H "Content-Type: application/json" \
-d '{
"client_name": "My MCP client",
"redirect_uris": ["http://127.0.0.1:33418/callback"],
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"],
"token_endpoint_auth_method": "none"
}'
| Field | Rules |
|---|---|
redirect_uris | Required, 1 to 10. Each must be an absolute https:// URL, or http:// on localhost, 127.0.0.1 or [::1], with no fragment. Custom schemes (such as cursor://) are refused. Redirect URIs are matched exactly. |
grant_types | Must include authorization_code; may add refresh_token. Default: both. |
response_types | ["code"]. |
token_endpoint_auth_method | none (public client, default), client_secret_post or client_secret_basic. A confidential client gets a client_secret once, in this answer. |
client_name | Up to 255 characters. Shown on the consent page, with a note that the app chose its own name. |
3. Authorization request
Send the person's browser to:
https://lantfly.com/api/oauth/authorize
?response_type=code
&client_id=CLIENT_ID
&redirect_uri=http%3A%2F%2F127.0.0.1%3A33418%2Fcallback
&scope=mcp
&state=RANDOM_STATE
&code_challenge=BASE64URL_SHA256_OF_VERIFIER
&code_challenge_method=S256
&resource=https%3A%2F%2Flantfly.com%2Fmcp
- PKCE with
S256is required for public clients. Confidential clients may leave it out. - The only scope is
mcp. resourceis optional and must name the MCP server.- An unknown client or a redirect URI that doesn't match shows an error page. Lantfly never redirects to an address the client didn't register.
4. Consent
Lantfly sends the browser to the Console's Connect an app page (https://lantfly.com/oauth/authorize). The person:
- signs in, if needed;
- sees which app asks and where access goes (the redirect host);
- picks the Account: their personal account or an organization. In an organization, they need the Create API tokens permission;
- optionally ticks Read-only: the app can search actors and read runs and results, but can't start runs or change anything;
- clicks Allow or Deny.
Allow redirects to the client's redirect_uri with a one-time code, the state and iss. Deny redirects with
error=access_denied. Lantfly emails the person a security notice when an app is connected. A request that isn't answered
in time expires; start again from the app.
5. Tokens
Exchange the code at POST /api/oauth/token (form-encoded):
curl -X POST https://lantfly.com/api/oauth/token \
-d grant_type=authorization_code \
-d code=AUTHORIZATION_CODE \
-d redirect_uri=http://127.0.0.1:33418/callback \
-d client_id=CLIENT_ID \
-d code_verifier=PKCE_VERIFIER
| Token | Lifetime | Notes |
|---|---|---|
| Authorization code | 10 minutes, one use | Using a code twice revokes the whole grant. |
| Access token | 1 hour | Send as Authorization: Bearer … to the MCP server. |
| Refresh token | 30 days, rotating | Each refresh returns a new refresh token. Sending a used refresh token again revokes the grant. |
Refresh with grant_type=refresh_token:
curl -X POST https://lantfly.com/api/oauth/token \
-d grant_type=refresh_token \
-d refresh_token=REFRESH_TOKEN \
-d client_id=CLIENT_ID
The token endpoint allows 30 requests a minute.
What the access token can do
- It acts for the account picked on the consent page, with the person's role there, and read-only if they chose so.
- It opens only the MCP server (
/mcpand/api/v3/mcp). Any other API path answers403 insufficient_permissions. - It ignores
X-Lantfly-Accountfor any other account: such a request answers403 forbidden. Connect the app again to use another organization. - Every exchange and refresh checks that the person is still a member of the organization. If not, the grant is revoked.
6. Revocation
| Who | How |
|---|---|
| The client | POST /api/oauth/revoke (RFC 7009) with its own access or refresh token. |
| You | Console → AI & MCP → Connected apps → Disconnect. The app stops working at once; connecting again asks you again. |
| Automatic | Changing or resetting your password disconnects every app. Leaving an organization ends that organization's grants. |
Connected apps list each app's name, the account it acts for, whether it's read-only, and when it was last used. Their hourly tokens don't appear under API tokens.