MCP - Headless and bot integrations

The interactive flow described in the MCP setup guide is the right path for IDEs and chat clients that can open a browser for OAuth. For unattended workloads — Discord bots, CI jobs, server processes — there are two supported patterns.

⚠️ The MCP server still relays every tool call through the html.to.design Figma plugin running on the same Figma account. Whichever Figma user owns the bot must keep the plugin open and signed in.

Endpoints at a glance

The MCP server is an OAuth 2.1 protected resource. Programmatic clients should read metadata from:

https://mcp.to.design/.well-known/oauth-protected-resource
https://mcp.to.design/.well-known/oauth-authorization-server

The authorization server publishes a standard discovery document with token_endpoint, registration_endpoint, grant_types_supported, etc.

ItemValue
Resourcehttps://mcp.to.design
Resource scopemcp
AS scopesopenid, offline_access, mcp
Grant typesauthorization_code, refresh_token, client_credentials
Access-token TTL1 hour
Refresh-token TTL14 days (non-rotating)
Dynamic Client RegistrationRFC 7591, public endpoint at registration_endpoint

Best when the bot operator can sign in once via a browser on any machine and then ship the refresh token to the headless process.

  1. Register a client (one-time) via Dynamic Client Registration:

    curl -X POST https://mcp.to.design/reg \
      -H 'Content-Type: application/json' \
      -d '{
        "client_name": "my-discord-bot",
        "redirect_uris": ["http://127.0.0.1:43117/callback"],
        "grant_types": ["authorization_code", "refresh_token"],
        "response_types": ["code"],
        "scope": "openid offline_access mcp"
      }'

    The response includes client_id, client_secret, and a registration_access_token for later updates.

  2. Do the auth-code dance once with PKCE, requesting scope=openid offline_access mcp and resource=https://mcp.to.design. You can do this on a workstation; only the resulting refresh token needs to move to the bot host.

  3. In the bot, exchange the refresh token for a fresh access token before each MCP call (or cache it for ~50 minutes):

    curl -X POST https://mcp.to.design/token \
      -u "$CLIENT_ID:$CLIENT_SECRET" \
      -d "grant_type=refresh_token&refresh_token=$REFRESH_TOKEN&resource=https://mcp.to.design"
  4. Call MCP with the access token in the Authorization: Bearer header, exactly like any other MCP client.

Refresh tokens are non-rotating with the default config, so the same value remains valid for the full 14-day TTL.

Pattern 2 — Service-account / client_credentials

Best when you cannot persist a user-derived refresh token, or you want the credential to be revocable independently of the human’s session.

A service account is a client_credentials OAuth client bound to a Figma user. The binding step requires the human to authenticate interactively once; after that, the bot uses the issued client_id/client_secret indefinitely.

  1. Sign in once interactively (via the same auth-code flow as Pattern 1) to obtain a normal user-scoped access token.

  2. Provision a service account by POSTing to /service-accounts with that user token:

    curl -X POST https://mcp.to.design/service-accounts \
      -H "Authorization: Bearer $USER_ACCESS_TOKEN"

    Response:

    {
      "client_id": "svc_…",
      "client_secret": "…",
      "token_endpoint": "https://mcp.to.design/token",
      "resource": "https://mcp.to.design",
      "scope": "mcp",
      "grant_type": "client_credentials"
    }

    Store the secret immediately — it is only returned once.

  3. In the bot, mint short-lived access tokens with the standard OAuth client_credentials grant:

    curl -X POST https://mcp.to.design/token \
      -u "$SVC_CLIENT_ID:$SVC_CLIENT_SECRET" \
      -d "grant_type=client_credentials&resource=https://mcp.to.design&scope=mcp"
  4. Call MCP with the access token as Authorization: Bearer …. The server resolves the bound Figma uid from the service-account record, so the call reaches the same Figma plugin instance as the human’s interactive session.

If the binding is missing, the server responds with 401 Unauthorized. The error_description is carried both in the WWW-Authenticate: Bearer … header and in the JSON response body ({"error": "unauthorized", "error_description": "service-account client is not bound to a Figma user…"}).

Token lifecycle and revocation

  • Access tokens: 1 hour. Always re-mint via /token, don’t cache aggressively.
  • Refresh tokens: 14 days, non-rotating. Replace by re-running the auth-code flow.
  • Revocation: the standard OAuth revocation endpoint is exposed at revocation_endpoint in the AS metadata. Revoking a service-account client is on the roadmap; in the meantime, email support@divriots.com to disable a svc_… client.