Skip to content

Commit 8f05930

Browse files
authored
Merge pull request #40 from mhmzdev/streamable-https
Streamable https
2 parents c494a85 + f80f1d5 commit 8f05930

File tree

5 files changed

+173
-23
lines changed

5 files changed

+173
-23
lines changed

.changeset/many-owls-shave.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"figma-flutter-mcp": patch
3+
---
4+
5+
StreamableHTTP and API key setup improved

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,14 @@
1818
"dev": "tsx src/cli.ts --http",
1919
"dev:stdio": "tsx src/cli.ts --stdio",
2020
"dev:port": "tsx src/cli.ts --http --port",
21+
"dev:remote": "tsx src/cli.ts --remote --port 3333",
2122
"changeset": "changeset add",
2223
"version": "changeset version && npm install --lockfile-only",
2324
"release": "changeset publish"
2425
},
2526
"dependencies": {
2627
"@modelcontextprotocol/sdk": "^1.0.0",
28+
"cors": "^2.8.5",
2729
"dotenv": "^17.2.1",
2830
"express": "^4.18.2",
2931
"node-fetch": "^3.3.2",
@@ -33,6 +35,7 @@
3335
"devDependencies": {
3436
"@changesets/changelog-github": "^0.5.1",
3537
"@changesets/cli": "^2.29.6",
38+
"@types/cors": "^2.8.17",
3639
"@types/express": "^4.17.21",
3740
"@types/node": "^20.10.0",
3841
"@types/node-fetch": "^2.6.9",

src/cli.ts

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,31 @@ async function startServer(): Promise<void> {
66
const config = getServerConfig();
77

88
if (config.isStdioMode) {
9-
await startMcpServer(config.figmaApiKey);
9+
await startMcpServer(config.figmaApiKey!);
1010
} else if (config.isHttpMode) {
11-
console.log('Starting Figma Flutter MCP Server in HTTP mode...');
11+
if (config.isRemoteMode) {
12+
console.log('Starting Figma Flutter MCP Server in REMOTE mode...');
13+
console.log('⚠️ Users MUST provide their own Figma API keys via:');
14+
console.log(' - Authorization header (Bearer token)');
15+
console.log(' - X-Figma-Api-Key header');
16+
console.log(' - figmaApiKey query parameter');
17+
console.log('📝 Get API key: https://help.figma.com/hc/en-us/articles/8085703771159-Manage-personal-access-tokens');
18+
} else {
19+
console.log('Starting Figma Flutter MCP Server in HTTP mode...');
20+
}
1221
await startHttpServer(config.httpPort, config.figmaApiKey);
1322
} else {
1423
console.log('Starting Figma Flutter MCP Server...');
15-
console.log('Use --stdio flag for MCP client communication');
16-
console.log('Use --http flag for local testing via HTTP');
24+
console.log('⚠️ You must provide your Figma API key via:');
25+
console.log(' • CLI argument: --figma-api-key=YOUR_KEY');
26+
console.log(' • Environment: FIGMA_API_KEY=YOUR_KEY in .env file');
27+
console.log('');
28+
console.log('Available modes:');
29+
console.log(' --stdio MCP client communication (requires API key)');
30+
console.log(' --http Local testing via HTTP (requires API key)');
31+
console.log(' --remote Remote deployment (users provide keys via HTTP headers)');
32+
console.log('');
33+
console.log('📝 Get your API key: https://help.figma.com/hc/en-us/articles/8085703771159-Manage-personal-access-tokens');
1734
console.log('Use --help for more options');
1835
}
1936
}

src/config.ts

Lines changed: 49 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,18 @@ import {hideBin} from "yargs/helpers";
44
import {resolve} from "path";
55

66
export interface ServerConfig {
7-
figmaApiKey: string;
7+
figmaApiKey?: string;
88
outputFormat: "yaml" | "json";
99
isStdioMode: boolean;
1010
isHttpMode: boolean;
11+
isRemoteMode: boolean;
1112
httpPort: number;
1213
configSources: {
13-
figmaApiKey: "cli" | "env";
14+
figmaApiKey: "cli" | "env" | "none";
1415
envFile: "cli" | "default";
1516
stdio: "cli" | "env" | "default";
1617
http: "cli" | "env" | "default";
18+
remote: "cli" | "env" | "default";
1719
port: "cli" | "env" | "default";
1820
};
1921
}
@@ -28,6 +30,7 @@ interface CliArgs {
2830
env?: string;
2931
stdio?: boolean;
3032
http?: boolean;
33+
remote?: boolean;
3134
port?: number;
3235
}
3336

@@ -37,7 +40,7 @@ export function getServerConfig(): ServerConfig {
3740
.options({
3841
"figma-api-key": {
3942
type: "string",
40-
description: "Figma API key",
43+
description: "Your Figma API key (can also be set via FIGMA_API_KEY env var)",
4144
},
4245
env: {
4346
type: "string",
@@ -53,6 +56,11 @@ export function getServerConfig(): ServerConfig {
5356
description: "Run in HTTP mode for local testing",
5457
default: false,
5558
},
59+
remote: {
60+
type: "boolean",
61+
description: "Run in remote mode - users provide their own Figma API keys",
62+
default: false,
63+
},
5664
port: {
5765
type: "number",
5866
description: "Port number for HTTP server",
@@ -79,28 +87,31 @@ export function getServerConfig(): ServerConfig {
7987
loadEnv({path: envFilePath, override: !!argv.env});
8088

8189
const config: ServerConfig = {
82-
figmaApiKey: "",
90+
figmaApiKey: undefined,
8391
outputFormat: "json",
8492
isStdioMode: false,
8593
isHttpMode: false,
94+
isRemoteMode: false,
8695
httpPort: 3333,
8796
configSources: {
88-
figmaApiKey: "env",
97+
figmaApiKey: "none",
8998
envFile: envFileSource,
9099
stdio: "default",
91100
http: "default",
101+
remote: "default",
92102
port: "default",
93103
},
94104
};
95105

96-
// Handle FIGMA_API_KEY
106+
// Handle FIGMA_API_KEY - Users must provide their own API key
97107
if (argv["figma-api-key"]) {
98108
config.figmaApiKey = argv["figma-api-key"];
99109
config.configSources.figmaApiKey = "cli";
100110
} else if (process.env.FIGMA_API_KEY) {
101111
config.figmaApiKey = process.env.FIGMA_API_KEY;
102112
config.configSources.figmaApiKey = "env";
103113
}
114+
// Users can provide API key via CLI args, .env file, or HTTP headers (in remote mode)
104115

105116
// Handle stdio mode
106117
if (argv.stdio) {
@@ -120,6 +131,17 @@ export function getServerConfig(): ServerConfig {
120131
config.configSources.http = "env";
121132
}
122133

134+
// Handle remote mode
135+
if (argv.remote) {
136+
config.isRemoteMode = true;
137+
config.isHttpMode = true; // Remote mode implies HTTP mode
138+
config.configSources.remote = "cli";
139+
} else if (process.env.REMOTE_MODE === "true") {
140+
config.isRemoteMode = true;
141+
config.isHttpMode = true; // Remote mode implies HTTP mode
142+
config.configSources.remote = "env";
143+
}
144+
123145
// Handle port configuration
124146
if (argv.port) {
125147
config.httpPort = argv.port;
@@ -129,21 +151,36 @@ export function getServerConfig(): ServerConfig {
129151
config.configSources.port = "env";
130152
}
131153

132-
// Validate configuration
133-
if (!config.figmaApiKey) {
134-
console.error("Error: FIGMA_API_KEY is required (via CLI argument or .env file)");
154+
// Validate configuration - Users must provide their own API key for ALL modes except remote
155+
if (!config.figmaApiKey && !config.isRemoteMode) {
156+
console.error("Error: FIGMA_API_KEY is required.");
157+
console.error("Please provide your Figma API key via one of these methods:");
158+
console.error(" 1. CLI argument: --figma-api-key=YOUR_API_KEY");
159+
console.error(" 2. Environment variable: FIGMA_API_KEY=YOUR_API_KEY in .env file");
160+
console.error("");
161+
console.error("Get your API key from: https://help.figma.com/hc/en-us/articles/8085703771159-Manage-personal-access-tokens");
162+
console.error("");
163+
console.error("Examples:");
164+
console.error(" npx figma-flutter-mcp --figma-api-key=YOUR_KEY --stdio");
165+
console.error(" echo 'FIGMA_API_KEY=YOUR_KEY' > .env && npx figma-flutter-mcp --stdio");
166+
console.error(" npx figma-flutter-mcp --remote # Users provide keys via HTTP headers");
135167
process.exit(1);
136168
}
137169

138170
// Log configuration sources (only in non-stdio mode)
139171
if (!config.isStdioMode) {
140172
console.log("\nConfiguration:");
141173
console.log(`- ENV_FILE: ${envFilePath} (source: ${config.configSources.envFile})`);
142-
console.log(
143-
`- FIGMA_API_KEY: ${maskApiKey(config.figmaApiKey)} (source: ${config.configSources.figmaApiKey})`
144-
);
174+
if (config.figmaApiKey) {
175+
console.log(
176+
`- FIGMA_API_KEY: ${maskApiKey(config.figmaApiKey)} (source: ${config.configSources.figmaApiKey})`
177+
);
178+
} else {
179+
console.log(`- FIGMA_API_KEY: Not set - users will provide their own (source: ${config.configSources.figmaApiKey})`);
180+
}
145181
console.log(`- STDIO_MODE: ${config.isStdioMode} (source: ${config.configSources.stdio})`);
146182
console.log(`- HTTP_MODE: ${config.isHttpMode} (source: ${config.configSources.http})`);
183+
console.log(`- REMOTE_MODE: ${config.isRemoteMode} (source: ${config.configSources.remote})`);
147184
if (config.isHttpMode) {
148185
console.log(`- HTTP_PORT: ${config.httpPort} (source: ${config.configSources.port})`);
149186
}

src/server.ts

Lines changed: 95 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { randomUUID } from "node:crypto";
22
import express, { type Request, type Response } from "express";
33
import { Server } from "http";
4+
import cors from "cors";
45
import {McpServer} from "@modelcontextprotocol/sdk/server/mcp.js";
56
import {StdioServerTransport} from "@modelcontextprotocol/sdk/server/stdio.js";
67
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
@@ -19,12 +20,44 @@ export function createServer(figmaApiKey: string) {
1920
return server;
2021
}
2122

23+
// Create a server instance that can handle per-user API keys
24+
export function createServerForUser(figmaApiKey: string) {
25+
return createServer(figmaApiKey);
26+
}
27+
2228
let httpServer: Server | null = null;
2329
const transports = {
2430
streamable: {} as Record<string, StreamableHTTPServerTransport>,
2531
sse: {} as Record<string, SSEServerTransport>,
2632
};
2733

34+
// Store MCP server instances per session (for per-user API keys)
35+
const sessionServers = {} as Record<string, McpServer>;
36+
37+
// Helper function to extract Figma API key from request
38+
function extractFigmaApiKey(req: Request, fallbackApiKey?: string): string | null {
39+
// Try to get from Authorization header (Bearer token)
40+
const authHeader = req.headers.authorization;
41+
if (authHeader && authHeader.startsWith('Bearer ')) {
42+
return authHeader.substring(7);
43+
}
44+
45+
// Try to get from custom header
46+
const figmaApiKey = req.headers['x-figma-api-key'] as string;
47+
if (figmaApiKey) {
48+
return figmaApiKey;
49+
}
50+
51+
// Try to get from query parameter (less secure, but convenient for testing)
52+
const queryApiKey = req.query.figmaApiKey as string;
53+
if (queryApiKey) {
54+
return queryApiKey;
55+
}
56+
57+
// Fall back to server-wide API key (only for non-remote HTTP mode)
58+
return fallbackApiKey || null;
59+
}
60+
2861
export async function startMcpServer(figmaApiKey: string): Promise<void> {
2962
try {
3063
const server = createServer(figmaApiKey);
@@ -37,10 +70,18 @@ export async function startMcpServer(figmaApiKey: string): Promise<void> {
3770
}
3871
}
3972

40-
export async function startHttpServer(port: number, figmaApiKey: string): Promise<void> {
41-
const mcpServer = createServer(figmaApiKey);
73+
export async function startHttpServer(port: number, figmaApiKey?: string): Promise<void> {
74+
// For remote mode, we don't create a single server instance
75+
// Instead, we create per-user servers based on their API keys
76+
// For non-remote HTTP mode, we use the provided API key
4277
const app = express();
4378

79+
// Configure CORS to expose Mcp-Session-Id header for browser-based clients
80+
app.use(cors({
81+
origin: '*', // Allow all origins - adjust as needed for production
82+
exposedHeaders: ['Mcp-Session-Id']
83+
}));
84+
4485
// Parse JSON requests for the Streamable HTTP endpoint only, will break SSE endpoint
4586
app.use("/mcp", express.json());
4687

@@ -49,39 +90,70 @@ export async function startHttpServer(port: number, figmaApiKey: string): Promis
4990
Logger.log("Received StreamableHTTP request");
5091
const sessionId = req.headers["mcp-session-id"] as string | undefined;
5192

93+
// Extract Figma API key from request
94+
const userFigmaApiKey = extractFigmaApiKey(req, figmaApiKey);
95+
if (!userFigmaApiKey) {
96+
res.status(401).json({
97+
jsonrpc: "2.0",
98+
error: {
99+
code: -32001,
100+
message: "Unauthorized: Figma API key required. You must provide your own Figma API key via Authorization header (Bearer token), X-Figma-Api-Key header, or figmaApiKey query parameter. Get your API key from: https://help.figma.com/hc/en-us/articles/8085703771159-Manage-personal-access-tokens",
101+
},
102+
id: null,
103+
});
104+
return;
105+
}
106+
52107
let transport: StreamableHTTPServerTransport;
108+
let mcpServer: McpServer;
53109

54110
if (sessionId && transports.streamable[sessionId]) {
55-
// Reuse existing transport
111+
// Reuse existing transport and server
56112
Logger.log("Reusing existing StreamableHTTP transport for sessionId", sessionId);
57113
transport = transports.streamable[sessionId];
114+
mcpServer = sessionServers[sessionId];
58115
} else if (isInitializeRequest(req.body)) {
59-
Logger.log("New initialization request for StreamableHTTP sessionId", sessionId);
116+
Logger.log("New initialization request for StreamableHTTP");
117+
118+
// Create new server instance for this user's API key
119+
mcpServer = createServerForUser(userFigmaApiKey);
120+
60121
transport = new StreamableHTTPServerTransport({
61122
sessionIdGenerator: () => randomUUID(),
62-
onsessioninitialized: (sessionId) => {
63-
// Store the transport by session ID
64-
transports.streamable[sessionId] = transport;
123+
enableJsonResponse: true, // Enable JSON response mode for better remote compatibility
124+
onsessioninitialized: (newSessionId) => {
125+
// Store the transport and server by session ID
126+
transports.streamable[newSessionId] = transport;
127+
sessionServers[newSessionId] = mcpServer;
128+
Logger.log("Session initialized with ID:", newSessionId);
65129
},
66130
});
67131
transport.onclose = () => {
68132
if (transport.sessionId) {
69133
delete transports.streamable[transport.sessionId];
134+
delete sessionServers[transport.sessionId];
70135
}
71136
};
72137
await mcpServer.connect(transport);
73138
} else if (sessionId) {
74139
// Session ID provided but transport not found - create new one
75140
Logger.log("Creating new transport for existing sessionId", sessionId);
141+
142+
// Create new server instance for this user's API key
143+
mcpServer = createServerForUser(userFigmaApiKey);
144+
76145
transport = new StreamableHTTPServerTransport({
77146
sessionIdGenerator: () => sessionId,
147+
enableJsonResponse: true, // Enable JSON response mode for better remote compatibility
78148
onsessioninitialized: (newSessionId) => {
79149
transports.streamable[newSessionId] = transport;
150+
sessionServers[newSessionId] = mcpServer;
80151
},
81152
});
82153
transport.onclose = () => {
83154
if (transport.sessionId) {
84155
delete transports.streamable[transport.sessionId];
156+
delete sessionServers[transport.sessionId];
85157
}
86158
};
87159
await mcpServer.connect(transport);
@@ -157,12 +229,28 @@ export async function startHttpServer(port: number, figmaApiKey: string): Promis
157229

158230
app.get("/sse", async (req, res) => {
159231
Logger.log("Establishing new SSE connection");
232+
233+
// Extract Figma API key from request
234+
const userFigmaApiKey = extractFigmaApiKey(req, figmaApiKey);
235+
if (!userFigmaApiKey) {
236+
res.status(401).json({
237+
error: "Unauthorized: Figma API key required. You must provide your own Figma API key via Authorization header (Bearer token), X-Figma-Api-Key header, or figmaApiKey query parameter. Get your API key from: https://help.figma.com/hc/en-us/articles/8085703771159-Manage-personal-access-tokens",
238+
});
239+
return;
240+
}
241+
160242
const transport = new SSEServerTransport("/messages", res);
161243
Logger.log(`New SSE connection established for sessionId ${transport.sessionId}`);
162244

245+
// Create server instance for this user's API key
246+
const mcpServer = createServerForUser(userFigmaApiKey);
247+
163248
transports.sse[transport.sessionId] = transport;
249+
sessionServers[transport.sessionId] = mcpServer;
250+
164251
res.on("close", () => {
165252
delete transports.sse[transport.sessionId];
253+
delete sessionServers[transport.sessionId];
166254
});
167255

168256
await mcpServer.connect(transport);

0 commit comments

Comments
 (0)