In this article
October 5, 2026
October 5, 2026

Figma MCP 403 Forbidden: Why client_name allowlists fail

igma's remote MCP server admits clients by the client_name they send during dynamic client registration. Why that check proves nothing, what CIMD fixes and what it doesn't, and what to gate on instead.

Explore with AI
Open in ChatGPT
Open in Claude
Open in Perplexity
Hitting this 403? Figma's remote MCP server only registers clients on its supported list. Your options: use a listed client, switch to the Figma desktop MCP server (it needs a Dev or Full seat on a paid plan and has no write-to-canvas), or ask to be added through Figma's MCP catalog waitlist.

Dynamic Client Registration (DCR) lets an OAuth client that has never met a server register itself and receive a client ID. Every field in that registration, including the client's name, is written by the client. A server that admits clients by name is trusting a label anyone can type.

Point opencode at Figma's remote MCP server, run opencode mcp auth figma, and the OAuth flow dies with an HTTP 403 whose entire body is the word Forbidden. There is nothing to parse and nothing to look up:

  
HTTP 403: Invalid OAuth error response: SyntaxError: JSON Parse error:
Unexpected identifier "Forbidden". Raw body: Forbidden
  

A developer reported that on Figma's forum in April 2026. The mechanism behind it is more interesting than the outage: Figma's authorization server is checking a string that the client fills in itself.

Why does Figma's MCP server return 403 Forbidden?

A Figma staff member explained it on the same thread in May: the remote MCP server allowlists client_name during dynamic client registration, opencode isn't on the list, and the mcp:connect scope is "intentionally gated to supported clients while we're in beta."

Dynamic Client Registration is how an MCP client that has never met a server gets a client ID. The client POSTs its own metadata (redirect_uris, client_name, grant_types, response_types, token_endpoint_auth_method), and the authorization server stores it and issues an ID. Every value in that request comes from the client. Figma reads client_name out of it and compares it against its supported-client list, which its help center says includes Claude Code, Claude Desktop, Cursor, VS Code, Codex, Gemini CLI, Kiro, Warp, Replit, and Xcode in beta, among others.

The same gate surfaces in three ways depending on how you approach it:

  • Register through DCR with an unlisted name and the registration endpoint returns a bare 403.
  • Request the scope as a normal third-party OAuth app and the MCP endpoint answers with www-authenticate: Bearer scope="mcp:connect", while the authorization request fails with "Invalid scopes for app".
  • Ask about policy and Figma's Community Support says MCP access is "currently limited to supported clients and integrations," pointing to a waitlist.

What sits behind the gate matters. Figma calls its remote server the preferred option, and write-to-canvas "requires the remote Figma MCP server." So the allowlist decides which agents can edit design files, not only read them.

The allowlist reads a label the client writes for itself, so anyone can carry an approved one.

Why isn't a client_name allowlist an identity check?

An allowlist keyed on self-asserted metadata sorts clients by honesty, not by identity. The Hacker News thread "Figma restricts MCP access to whitelisted clients, excluding Pi" passed 180 points and 100 comments on October 1, and worked that out in a few replies. One commenter put the bypass plainly:

"Rather than constraining the callback url to pre-registered partners, you just need to enter 'Claude Code' as your product name"

Another explained why the usual OAuth guardrail does nothing here:

"callback urls are all localhost, there's no domain to whitelist, just client names"

Then came the demonstrations. A Pi user reported that after Pi shipped an OAuth client-name field, "I just wrote Codex and Figma mcp works now." A public repository now automates the same trick by registering as "Claude Code." And GitHub's own Copilot CLI hit the wall from the other side: an open issue shows registration with client_name: "copilot-cli" rejected with a 403, while "GitHub Copilot CLI" passes. Same client, same code, different string.

The OpenID Foundation's paper on identity for agentic AI, published in October 2025, named this failure in the general case:

"The MCP protocol's approach to scalability leveraged Dynamic Client Registration, allowing any client to register with a server and obtain credentials. While this model offers frictionless onboarding in a "many-to-many" ecosystem, it introduces a critical security flaw: it creates a large number of anonymous clients."

The same paper is blunt that a client ID generated during dynamic client registration "is not a robust stand-alone workload identity." Figma built its access policy on top of exactly that. The policy excludes clients that describe themselves accurately and admits anything willing to type a different name. That is lock-in, without the assurance that is supposed to come with it.

What does onboarding look like today?

The second cost is procedural. Figma's Community Support tells developers that "we're being intentional about the integrations we support in the near term," and that the way in is the MCP catalog waitlist, Figma Support, or your account representative. On the opencode thread, Figma suggested developers upvote a GitHub issue and later said it was "actively working on it," with no ETA. On Hacker News, opencode's maintainer was quoted saying "we've had an email thread going on for 8 months trying to get it setup in opencode."

Granularity suffers too. GitHub's Copilot CLI works under the right name, while the Copilot desktop app gets a 403. An internal AWS Bedrock AgentCore gateway, neither a public client nor an IDE plugin, hits the same wall, and Figma's support answer is that there is no self-serve path for internal clients. That is the predictable result when access is a hand-maintained list of display names rather than a policy evaluated against something the server can check.

A partnership program is a legitimate business decision. Running one through OAuth's registration endpoint, where developers expect a protocol, is how you end up with a 403 whose body is one word.

Is the allowlist a reasonable tradeoff?

The strongest defense of Figma's position came from a commenter who runs security review at their own company and reached the same design:

"we decided to do an allowlist pattern because it was a reasonable tradeoff. The solution is allowing per-tenant client configuration, but that comes with its own set of issues"

That tradeoff is real, and the context supports it. Figma's MCP server is in beta and free, with Figma saying it "will eventually be a usage-based paid feature," and write access to customer design files is not a surface you open casually. Gating is defensible. Gating on a field the client fills in itself is not.

What did the MCP spec change?

MCP's 2026-07-28 revision deprecated Dynamic Client Registration:

"Dynamic Client Registration is deprecated. New implementations should use Client ID Metadata Documents instead. This option remains available for backwards compatibility with authorization servers that do not support Client ID Metadata Documents."

Clients now follow a priority order: pre-registered client information first, then Client ID Metadata Documents (CIMD) if the authorization server advertises client_id_metadata_document_supported, then DCR as a fallback, then prompting the user. Under CIMD, the client_id is an HTTPS URL with a path component, and the document it points to must include at least client_id, client_name, and redirect_uris. The spec's own example:

  
{
  "client_id": "https://app.example.com/oauth/client-metadata.json",
  "client_name": "Example MCP Client",
  "client_uri": "https://app.example.com",
  "logo_uri": "https://app.example.com/logo.png",
  "redirect_uris": [
    "http://127.0.0.1:3000/callback",
    "http://localhost:3000/callback"
  ],
  "grant_types": ["authorization_code"],
  "response_types": ["code"],
  "token_endpoint_auth_method": "none"
}
  

The authorization server fetches that URL, MUST check that the document's client_id matches the URL exactly, and MUST validate the redirect URI in the authorization request against the list in the document. Because the identifier is a URL the client hosts, it is portable: "No re-registration is needed when the authorization server changes."

Nobody has to break existing partners to get there. A server can advertise CIMD support and keep its registration endpoint running for clients that haven't moved, which is the backwards compatibility the spec deliberately preserves. The allowlist can survive the migration; what changes is the field it reads.

Does CIMD prove which client is connecting?

Partly, and the part it doesn't prove is exactly where Figma's problem lives.

For a hosted client with an HTTPS redirect URI, CIMD gives you a real anchor. The client's identity is a document on a domain someone controls, and an impostor who presents that client's metadata URL never receives the authorization code, because the code goes to the real app's HTTPS redirect. As WorkOS has put it, "CIMD proves you control a domain, not that you're trustworthy," and for an allowlist, domain control is a far better key than a typed name.

For CLIs and desktop apps, the clients this whole dispute is about, the redirect URI is localhost. The spec says so directly: "Client ID Metadata Documents cannot prevent localhost URL impersonation by themselves." Any local program can present Claude Code's metadata URL and listen on the loopback port the code is sent to. The spec's answer is to warn users about localhost-only redirect URIs, "clearly display the redirect URI hostname during authorization," and optionally require additional attestation.

That changes the conclusion. For distributed native clients, no registration method proves which binary is calling. CIMD makes the identity claim honest and stable, but it can't make an allowlist of public CLIs enforceable. Figma's list doesn't fail because it picked the wrong field. It fails because it is trying to enforce something the protocol can't prove for these clients.

Registration methodWhat it proves about the clientCan an impostor pass?
DCR, server checks client_nameNothing. The name is typed by the clientYes, by typing an allowlisted name
CIMD, HTTPS redirect URIThe client controls the domain hosting its metadata and redirectNo. The code goes to the real app's redirect
CIMD, localhost redirect URIA metadata document exists at that URLYes. Any local program can listen on the loopback port
CIMD plus private_key_jwtThe client holds the private key behind its published JWKSOnly with the key, so it fits hosted clients, not CLIs that ship to every laptop
Pre-registrationThe server issued credentials to a known partyOnly if the credentials leak, which public native clients can't fully prevent
Three-panel comparison of what an MCP server can rely on when a client registers. With dynamic client registration and a client_name check, the server trusts a string typed into the request, which proves nothing. With a Client ID Metadata Document and an HTTPS redirect URI, the server fetches metadata from a URL the client's owner controls, which proves domain control. With a Client ID Metadata Document and a localhost redirect URI, any local program can claim the metadata URL and catch the authorization code, so the server should gate on the user, organization and scopes instead.

What should an MCP server gate on instead?

Verify the client where you can, and say so where you can't. Accept URL-formatted client IDs, fetch the document, and enforce the exact client_id match and the redirect URI check. For hosted clients with HTTPS redirects, key your policy on that domain. For localhost-only clients, treat client identity as a claim, show the redirect host and a warning on the consent screen as the spec requires, and don't hang write access on it.

Tier on scopes, not names. A bare 403 is the least informative answer a server can give. The spec separates "token invalid" (401) from "invalid scopes or insufficient permissions" (403) and tells servers to name the required scopes in the WWW-Authenticate challenge. A signed-in user on an unknown client can hold read scopes while write-to-canvas sits behind a higher tier, and the client learns exactly which scope it lacks:

  
HTTP/1.1 403 Forbidden
WWW-Authenticate: Bearer error="insufficient_scope",
                         scope="canvas:write",
                         resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource",
                         error_description="Write access requires an approved client tier"
  

Let the customer's admin decide. The commenter defending allowlists named the real fix: "per-tenant client configuration." Write access to a company's design files is that company's call. A per-organization policy, set by the customer's admin, for which clients may hold which scopes puts the decision with the party that owns the data, instead of a vendor list that every new tool has to petition.

  
type Tier = "read" | "write";

async function maxTier(clientId: string, orgId: string): Promise<Tier> {
  const client = await resolveClient(clientId);        // CIMD fetch + exact client_id match
  const policy = await getOrgClientPolicy(orgId);      // set by the customer's admin

  // A client the org has explicitly approved gets write, wherever it runs.
  if (policy.approvedClients.includes(client.id)) return "write";

  // Hosted clients with verified HTTPS redirects can earn write by org policy.
  if (client.httpsRedirectsOnly && policy.allowVerifiedHostedClients) return "write";

  // Everyone else, including localhost-only CLIs, gets read until approved.
  return "read";
}
  

Publish the path to the next tier. Stated requirements, a visible queue, and a self-serve way for an organization to approve the tools its own people use. "Email your account representative" and an eight-month thread are not an onboarding process. They are a bottleneck that scales with headcount.

Key revocation and audit on the same identifiers. Log the client ID, user, organization, and scope behind every grant and every write. A bad client can then be denied by its client ID and by org policy, rather than by a display name anyone can retype. That is the lifecycle and audit discipline the OpenID Foundation paper asks for, where audit logs record "not only who authorized an action but also which specific agent instance performed it."

WorkOS implements the server half of this: if an MCP client shows up with a URL-style client ID, "AuthKit will fetch, validate, and cache that metadata per the 2025-11-25 spec, no Dynamic Client Registration required." The mechanics of both registration patterns are covered in CIMD vs DCR, Client ID Metadata Documents in MCP, and where DCR still makes sense.

Figma's beta will end, and the features behind this gate are slated to become a paid, usage-based product. At that point the allowlist stops being a beta guardrail and becomes the access-control model customers are paying for. The first security questionnaire will ask what proves a client is the client it claims to be. For a list of CLI names, the honest answer is nothing, and the better answer is to stop asking that question of the client and start asking it of the user, the organization, and the scopes they were granted.