OAuth for apps
Crateyard is an OAuth 2.1 authorization server for MCP clients such as ChatGPT. Clients use the authorization code flow with PKCE (S256) and get short-lived bearer tokens for the MCP endpoint. Users approve each app on a consent page and can disconnect it at any time in Settings > Connected Apps.
These endpoints are off unless the server sets OAUTH_SERVER_ENABLED=true. When off, every endpoint here returns 404.
Placeholders below: https://crateyard.com is the site origin, for example https://crateyard.example.
Scopes
assets:read: list and view assets, tasks, pixel sprites, pixel animations, and the account name.assets:write: create and rename assets. No tokens are spent.generate: start image, 3D, and pixel animation generations. These spend the user's app tokens.
If a client asks for no scope, it gets assets:read assets:write. generate is only granted when requested and approved.
GET /.well-known/oauth-protected-resource
Protected resource metadata (RFC 9728). Also served at /.well-known/oauth-protected-resource/mcp. No authentication.
{
"resource": "https://crateyard.com/mcp",
"authorization_servers": ["https://crateyard.com"],
"scopes_supported": ["assets:read", "assets:write", "generate"],
"bearer_methods_supported": ["header"],
"resource_documentation": "https://crateyard.com/docs/api"
}GET /.well-known/oauth-authorization-server
Authorization server metadata (RFC 8414). No authentication.
{
"issuer": "https://crateyard.com",
"authorization_response_iss_parameter_supported": true,
"authorization_endpoint": "https://crateyard.com/oauth/authorize",
"token_endpoint": "https://crateyard.com/oauth/token",
"registration_endpoint": "https://crateyard.com/oauth/register",
"revocation_endpoint": "https://crateyard.com/oauth/revoke",
"response_types_supported": ["code"],
"grant_types_supported": ["authorization_code", "refresh_token"],
"token_endpoint_auth_methods_supported": ["none"],
"code_challenge_methods_supported": ["S256"],
"scopes_supported": ["assets:read", "assets:write", "generate"]
}POST /oauth/register
Dynamic client registration (RFC 7591). Public clients only. No authentication. Rate limit: 300 registrations per IP per hour.
Request body (JSON):
redirect_uris(required): 1 to 10httpsURLs without fragments.http://localhostis accepted outside production.client_name(optional): shown on the consent page.token_endpoint_auth_method(optional): must benoneif present.
curl -X POST "https://crateyard.com/oauth/register" \
-H "Content-Type: application/json" \
-d '{"client_name":"My MCP client","redirect_uris":["https://client.example/callback"]}'Response 201:
{
"client_id": "cy_client_...",
"client_id_issued_at": 1760000000,
"client_name": "My MCP client",
"redirect_uris": ["https://client.example/callback"],
"token_endpoint_auth_method": "none",
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"]
}Errors: 400 invalid_redirect_uri, 400 invalid_client_metadata, 429 temporarily_unavailable.
GET /oauth/authorize
Opens the consent page in the user's browser. Users who are not signed in go to sign in (or sign up) first and then come back.
Query parameters: client_id, redirect_uri (must exactly match a registered URI), response_type=code, code_challenge, code_challenge_method=S256, state (recommended), scope (optional), resource (optional, must be https://crateyard.com/mcp).
After the user decides, the browser is redirected to redirect_uri with:
- Approved:
code,state,iss. - Declined:
error=access_denied,state,iss.
Every redirect carries iss (RFC 9207). Codes are single use and expire after 10 minutes. Problems with client_id or redirect_uri show an error page and never redirect. Other problems redirect with error set to invalid_request, invalid_scope, invalid_target, or unsupported_response_type.
POST /oauth/token
Form-encoded or JSON. No client secret. Rate limit: 600 requests per minute per client ID and IP pair. Exceeding the limit returns 429 with Retry-After.
Authorization code exchange:
curl -X POST "https://crateyard.com/oauth/token" \
-d grant_type=authorization_code \
-d client_id=cy_client_example \
-d code=cy_ac_example \
-d redirect_uri=https://client.example/callback \
-d code_verifier=YOUR_PKCE_VERIFIER \
-d resource=https://crateyard.com/mcpRefresh:
curl -X POST "https://crateyard.com/oauth/token" \
-d grant_type=refresh_token \
-d client_id=cy_client_example \
-d refresh_token=cy_rt_exampleResponse 200:
{
"access_token": "cy_at_...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "cy_rt_...",
"scope": "assets:read assets:write"
}Access tokens last 1 hour. Refresh tokens last 30 days and rotate: each refresh returns a new pair and the old refresh token stops working. Presenting an already used code or refresh token revokes the whole connection, because it means the token leaked.
Errors (400 unless noted): invalid_request, invalid_grant, invalid_target, invalid_scope, unsupported_grant_type, 401 invalid_client, 429 temporarily_unavailable (120 requests per IP per minute).
POST /oauth/revoke
RFC 7009. Body: token (access or refresh token). Revoking any token disconnects the app for that user. Always returns 200, even for unknown tokens.
Token usage
OAuth endpoints never spend tokens. Only MCP tools with the generate scope do. See ChatGPT app.