Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions apps/mesh/src/api/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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();
Expand Down
15 changes: 15 additions & 0 deletions apps/mesh/src/core/provider-helpers.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -528,6 +528,20 @@ export function CreateConnectionDialog({
</a>
</>
)}
{providerHint.id === "figma" && (
<>
{" "}
·{" "}
<a
className="text-foreground underline underline-offset-4 hover:text-foreground/80"
href="https://www.figma.com/developers/api#access-tokens"
target="_blank"
rel="noreferrer"
>
Open Figma PAT settings
</a>
</>
)}
</p>
)}
<FormMessage />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -340,7 +352,11 @@ export function ConnectionFields({
render={({ field }) => (
<FormItem className="flex flex-col gap-3">
<FormLabel className="text-xs text-muted-foreground font-medium">
{isGitHubCopilotMcp ? "GitHub Personal Access Token" : "Token"}
{isGitHubCopilotMcp
? "GitHub Personal Access Token"
: isFigmaMcp
? "Figma Personal Access Token"
: "Token"}
</FormLabel>
{/* Authentication status badge */}
{hasOAuthToken ? (
Expand Down Expand Up @@ -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 || ""}
Expand All @@ -415,6 +433,20 @@ export function ConnectionFields({
</a>
</FormDescription>
)}
{isFigmaMcp && (
<FormDescription>
Create a PAT at{" "}
<a
href="https://www.figma.com/developers/api#access-tokens"
target="_blank"
rel="noopener noreferrer"
className="underline"
>
figma.com/developers
</a>
. Required for MCP OAuth registration.
</FormDescription>
)}
</>
)}
<FormMessage />
Expand Down
24 changes: 23 additions & 1 deletion apps/mesh/src/web/utils/connection-form-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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?: {
Expand Down Expand Up @@ -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") {
Expand Down
Loading