From 736209e471a09ebc3b3210ee651b5eeb5dc56ddf Mon Sep 17 00:00:00 2001
From: Jonas Jesus
Date: Mon, 13 Apr 2026 16:11:28 -0300
Subject: [PATCH 1/2] feat(oauth-proxy): add Figma MCP support with PAT-based
registration
Figma Dynamic Client Registration requires an X-Figma-Token header and
specific client_name/scope values. This adds provider-specific handling
in the OAuth proxy to inject the PAT and rewrite the registration body,
plus UI hints for Figma connections.
Co-Authored-By: Claude Opus 4.6
---
apps/mesh/src/api/app.ts | 31 ++++++++++++++++
apps/mesh/src/core/provider-helpers.ts | 15 ++++++++
.../connections/create-connection-dialog.tsx | 14 ++++++++
.../details/connection/connection-sidebar.tsx | 36 +++++++++++++++++--
.../src/web/utils/connection-form-helpers.ts | 24 ++++++++++++-
5 files changed, 117 insertions(+), 3 deletions(-)
create mode 100644 apps/mesh/src/core/provider-helpers.ts
diff --git a/apps/mesh/src/api/app.ts b/apps/mesh/src/api/app.ts
index d879b28894..8eacd72e2e 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,16 @@ 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)";
+ body.scope = body.scope || "mcp:connect";
+ requestBody = JSON.stringify(body);
} else {
// For other content types, pass through as-is
requestBody = c.req.raw.body ?? undefined;
@@ -742,6 +760,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
+
+ >
+ )}
)}
diff --git a/apps/mesh/src/web/components/details/connection/connection-sidebar.tsx b/apps/mesh/src/web/components/details/connection/connection-sidebar.tsx
index 4413a2b42d..bbe2ded6c7 100644
--- a/apps/mesh/src/web/components/details/connection/connection-sidebar.tsx
+++ b/apps/mesh/src/web/components/details/connection/connection-sidebar.tsx
@@ -72,6 +72,18 @@ export function ConnectionFields({
}
})();
+ const isFigmaMcp = (() => {
+ if (typeof connectionUrl !== "string" || !connectionUrl) return false;
+ try {
+ const url = new URL(connectionUrl);
+ return (
+ url.hostname === "figma.com" || url.hostname.endsWith(".figma.com")
+ );
+ } catch {
+ return false;
+ }
+ })();
+
const showStdioOptions =
stdioEnabled || connection.connection_type === "STDIO";
@@ -340,7 +352,11 @@ export function ConnectionFields({
render={({ field }) => (
- {isGitHubCopilotMcp ? "GitHub Personal Access Token" : "Token"}
+ {isGitHubCopilotMcp
+ ? "GitHub Personal Access Token"
+ : isFigmaMcp
+ ? "Figma Personal Access Token"
+ : "Token"}
{/* Authentication status badge */}
{hasOAuthToken ? (
@@ -395,7 +411,9 @@ export function ConnectionFields({
placeholder={
isGitHubCopilotMcp
? "Paste your GitHub PAT"
- : "Enter access token..."
+ : isFigmaMcp
+ ? "Paste your Figma PAT"
+ : "Enter access token..."
}
{...field}
value={field.value || ""}
@@ -415,6 +433,20 @@ export function ConnectionFields({
)}
+ {isFigmaMcp && (
+
+ Create a PAT at{" "}
+
+ figma.com/developers
+
+ . Required for MCP OAuth registration.
+
+ )}
>
)}
diff --git a/apps/mesh/src/web/utils/connection-form-helpers.ts b/apps/mesh/src/web/utils/connection-form-helpers.ts
index be75f5a555..87b652e259 100644
--- a/apps/mesh/src/web/utils/connection-form-helpers.ts
+++ b/apps/mesh/src/web/utils/connection-form-helpers.ts
@@ -8,7 +8,7 @@ import type { RegistryItem } from "@/web/components/store/types";
// ---------------------------------------------------------------------------
export type ConnectionProviderHint = {
- id: "github" | "perplexity" | "registry";
+ id: "github" | "figma" | "perplexity" | "registry";
title?: string;
description?: string | null;
token?: {
@@ -86,6 +86,28 @@ export function inferHardcodedProviderHint(params: {
};
}
+ // Figma MCP (hardcoded)
+ if (uiType === "HTTP" || uiType === "SSE" || uiType === "Websocket") {
+ try {
+ const url = new URL(normalized);
+ if (url.hostname === "figma.com" || url.hostname.endsWith(".figma.com")) {
+ return {
+ id: "figma",
+ title: "Figma",
+ description: "Figma MCP",
+ token: {
+ label: "Figma PAT",
+ placeholder: "figd_…",
+ helperText:
+ "Paste a Figma Personal Access Token — required for OAuth registration",
+ },
+ };
+ }
+ } catch {
+ // invalid URL, skip
+ }
+ }
+
// Perplexity MCP (hardcoded)
const npxPackage = (params.npxPackage ?? "").trim();
if (uiType === "NPX" && npxPackage === "@perplexity-ai/mcp-server") {
From e50f17ce1b4ecdebce6a60e42c8079d87de74796 Mon Sep 17 00:00:00 2001
From: Jonas Jesus
Date: Mon, 13 Apr 2026 16:32:18 -0300
Subject: [PATCH 2/2] fix(oauth-proxy): ensure mcp:connect scope is always
present for Figma register
Co-Authored-By: Claude Opus 4.6
---
apps/mesh/src/api/app.ts | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/apps/mesh/src/api/app.ts b/apps/mesh/src/api/app.ts
index 8eacd72e2e..b8162d8849 100644
--- a/apps/mesh/src/api/app.ts
+++ b/apps/mesh/src/api/app.ts
@@ -742,7 +742,12 @@ export async function createApp(options: CreateAppOptions = {}) {
// Figma: rewrite client_name and ensure scope for registration
const body = await c.req.json();
body.client_name = "Claude Code (figma)";
- body.scope = body.scope || "mcp:connect";
+ 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