diff --git a/apps/mesh/src/api/app.ts b/apps/mesh/src/api/app.ts index d879b28894..b8162d8849 100644 --- a/apps/mesh/src/api/app.ts +++ b/apps/mesh/src/api/app.ts @@ -10,6 +10,7 @@ import { getSettings } from "../settings"; import { DECO_STORE_URL, isDecoHostedMcp } from "@/core/deco-constants"; +import { isFigmaConnection } from "@/core/provider-helpers"; import { WellKnownOrgMCPId } from "@decocms/mesh-sdk"; import { PrometheusSerializer } from "@opentelemetry/exporter-prometheus"; import { Hono } from "hono"; @@ -707,6 +708,13 @@ export async function createApp(options: CreateAppOptions = {}) { const authorization = c.req.header("Authorization"); if (authorization) headers["Authorization"] = authorization; + // Figma MCP: inject X-Figma-Token header for register endpoint + // Figma's Dynamic Client Registration requires a Personal Access Token + const isFigma = isFigmaConnection(connection.connection_url); + if (isFigma && endpoint === "register" && connection.connection_token) { + headers["X-Figma-Token"] = connection.connection_token; + } + // For token endpoint, we may need to rewrite the 'resource' parameter in the body // (same reason as authorize: auth servers validate it's their actual endpoint) let requestBody: BodyInit | undefined; @@ -726,6 +734,21 @@ export async function createApp(options: CreateAppOptions = {}) { params.append(key, value.toString()); } requestBody = params.toString(); + } else if ( + isFigma && + endpoint === "register" && + contentType?.includes("application/json") + ) { + // Figma: rewrite client_name and ensure scope for registration + const body = await c.req.json(); + body.client_name = "Claude Code (figma)"; + const scopes = + typeof body.scope === "string" + ? body.scope.split(/\s+/).filter(Boolean) + : []; + if (!scopes.includes("mcp:connect")) scopes.push("mcp:connect"); + body.scope = scopes.join(" "); + requestBody = JSON.stringify(body); } else { // For other content types, pass through as-is requestBody = c.req.raw.body ?? undefined; @@ -742,6 +765,19 @@ export async function createApp(options: CreateAppOptions = {}) { redirect: "manual", }); + // Figma: enhance 403 errors on register with actionable message + if (isFigma && endpoint === "register" && response.status === 403) { + return c.json( + { + error: "Figma registration failed", + error_description: connection.connection_token + ? "Figma rejected the PAT. Verify it hasn't expired and has the correct scopes." + : "Figma requires a PAT for MCP registration. Add your Figma PAT in the connection settings.", + }, + 403, + ); + } + // Copy response headers, excluding hop-by-hop and encoding headers // Note: Node.js fetch auto-decompresses, so content-encoding/content-length would be wrong const responseHeaders = new Headers(); diff --git a/apps/mesh/src/core/provider-helpers.ts b/apps/mesh/src/core/provider-helpers.ts new file mode 100644 index 0000000000..e936d5a05b --- /dev/null +++ b/apps/mesh/src/core/provider-helpers.ts @@ -0,0 +1,15 @@ +/** + * Provider-specific helpers for MCP connections. + * Detects well-known providers that need special handling in the OAuth proxy. + */ + +/** Check if a connection URL points to Figma's MCP server */ +export function isFigmaConnection(connectionUrl: string | null): boolean { + if (!connectionUrl) return false; + try { + const url = new URL(connectionUrl); + return url.hostname === "figma.com" || url.hostname.endsWith(".figma.com"); + } catch { + return false; + } +} diff --git a/apps/mesh/src/web/components/connections/create-connection-dialog.tsx b/apps/mesh/src/web/components/connections/create-connection-dialog.tsx index da4103a426..fde4272372 100644 --- a/apps/mesh/src/web/components/connections/create-connection-dialog.tsx +++ b/apps/mesh/src/web/components/connections/create-connection-dialog.tsx @@ -528,6 +528,20 @@ export function CreateConnectionDialog({ > )} + {providerHint.id === "figma" && ( + <> + {" "} + ·{" "} + + Open Figma PAT settings + + > + )}
)}