1use rmcp::{
2 handler::server::{router::tool::ToolRouter, tool::Parameters},
3 model::*,
4 schemars, tool, tool_handler, tool_router,
5 transport::stdio,
6 service::{RequestContext, RoleServer},
7 Error as McpError, ServerHandler, ServiceExt,
8};
9use serde::Deserialize;
10use std::future::Future;
11use base64::{Engine as _, engine::general_purpose};
12
13use crate::{
14 figma::{FigmaClient, FigmaUrlParser, ImageCache},
15 Error,
16};
17
18#[derive(Clone)]
19pub struct FigmaServer {
20 client: FigmaClient,
21 url_parser: FigmaUrlParser,
22 image_cache: ImageCache,
23 tool_router: ToolRouter<FigmaServer>,
24}
25
26#[tool_router]
27impl FigmaServer {
28 pub fn new(figma_token: String) -> std::result::Result<Self, Error> {
29 let client = FigmaClient::new(figma_token)?;
30 let url_parser = FigmaUrlParser::new();
31
32 Ok(Self {
33 client,
34 url_parser,
35 image_cache: ImageCache::new(),
36 tool_router: Self::tool_router(),
37 })
38 }
39
40 pub async fn run_stdio(self) -> std::result::Result<(), Error> {
41 tracing::info!("Starting Figma MCP server");
42
43 let service = self.serve(stdio()).await.map_err(|e| {
44 tracing::error!("Failed to start MCP service: {:?}", e);
45 Error::Mcp(e.into())
46 })?;
47
48 tracing::info!("MCP service started successfully, waiting for connections");
49 service.waiting().await.map_err(|e| {
50 tracing::error!("MCP service error: {:?}", e);
51 Error::Mcp(e.into())
52 })?;
53
54 Ok(())
55 }
56
57 #[tool(description = "Parse a Figma URL to extract IDs and determine the URL type")]
58 async fn parse_figma_url(
59 &self,
60 Parameters(ParseUrlRequest { url }): Parameters<ParseUrlRequest>,
61 ) -> Result<CallToolResult, McpError> {
62 let url_info = match self.url_parser.parse(&url) {
63 Ok(parsed) => parsed,
64 Err(e) => {
65 let error_msg = format!("Error parsing URL: {}", e);
66 return tool_error(error_msg);
67 }
68 };
69
70 let result = serde_json::to_string_pretty(&url_info)
71 .unwrap_or_else(|e| format!("Serialization error: {}", e));
72
73 tool_success(result)
74 }
75
76 #[tool(description = "Get file contents from a Figma file using file key")]
77 async fn get_file(
78 &self,
79 Parameters(GetFileRequest { file_key, depth }): Parameters<GetFileRequest>,
80 ) -> Result<CallToolResult, McpError> {
81 let depth = depth.unwrap_or(1);
82 let result = match self.client.get_file(&file_key, Some(depth)).await {
83 Ok(file) => file,
84 Err(e) => {
85 let error_msg = format!("Error fetching file: {}", e);
86 return tool_error(error_msg);
87 }
88 };
89
90 let result = serde_json::to_string_pretty(&result)
91 .unwrap_or_else(|e| format!("Serialization error: {}", e));
92
93 tool_success(result)
94 }
95
96 #[tool(description = "Get specific nodes from a file using file key")]
97 async fn get_file_nodes(
98 &self,
99 Parameters(GetFileNodesRequest {
100 file_key,
101 node_ids,
102 depth,
103 }): Parameters<GetFileNodesRequest>,
104 ) -> Result<CallToolResult, McpError> {
105 let node_ids: Vec<String> = node_ids.split(',').map(|s| s.trim().to_string()).collect();
106 let depth = depth.unwrap_or(1);
107
108 let result = match self
109 .client
110 .get_file_nodes(&file_key, &node_ids, Some(depth))
111 .await
112 {
113 Ok(nodes) => nodes,
114 Err(e) => {
115 let error_msg = format!("Error fetching file nodes: {}", e);
116 return tool_error(error_msg);
117 }
118 };
119
120 let result = serde_json::to_string_pretty(&result)
121 .unwrap_or_else(|e| format!("Serialization error: {}", e));
122
123 tool_success(result)
124 }
125
126 #[tool(description = "Export images from a Figma file using file key")]
127 async fn export_images(
128 &self,
129 Parameters(ExportImageRequest {
130 file_key,
131 node_ids,
132 format,
133 scale,
134 }): Parameters<ExportImageRequest>,
135 ) -> Result<CallToolResult, McpError> {
136 let node_ids_to_export: Vec<String> =
137 node_ids.split(',').map(|s| s.trim().to_string()).collect();
138
139 let format = format.as_deref().unwrap_or("png");
140 let scale_value = scale.unwrap_or(1.0);
141
142 let result = match self
143 .client
144 .export_images(&file_key, &node_ids_to_export, format, scale)
145 .await
146 {
147 Ok(export_result) => export_result,
148 Err(e) => {
149 let error_msg = format!("Error exporting images: {}", e);
150 return tool_error(error_msg);
151 }
152 };
153
154 if let Some(images) = result.get("images").and_then(|v| v.as_object()) {
156 for (node_id, url) in images {
157 if let Some(url_str) = url.as_str() {
158 let _ = self.image_cache.register_export(
159 file_key.clone(),
160 node_id.clone(),
161 format.to_string(),
162 scale_value,
163 url_str.to_string(),
164 );
165 }
166 }
167 }
168
169 let result = serde_json::to_string_pretty(&result)
170 .unwrap_or_else(|e| format!("Serialization error: {}", e));
171
172 tool_success(result)
173 }
174
175 #[tool(description = "Get current user information (useful for testing authentication)")]
176 async fn get_me(&self) -> Result<CallToolResult, McpError> {
177 let result = match self.client.get_me().await {
178 Ok(user) => user,
179 Err(e) => {
180 let error_msg = format!("Error fetching user info: {}", e);
181 return tool_error(error_msg);
182 }
183 };
184
185 let result = serde_json::to_string_pretty(&result)
186 .unwrap_or_else(|e| format!("Serialization error: {}", e));
187
188 tool_success(result)
189 }
190
191 #[tool(description = "Help: How to use this Figma file MCP server")]
192 async fn help(&self) -> Result<CallToolResult, McpError> {
193 let help_text = r#"
194# Figma MCP Server Help
195
196This MCP server provides tools to access and work with Figma files using file keys with depth control to manage response size.
197
198## Workflow
199
2001. First, use `parse_figma_url` to extract the file key from a Figma URL
2012. Then use the file key with other tools to access file data
2023. Use the depth parameter to control how much data is returned and avoid token limits
2034. Navigate deeper into the file structure using recursive calls with specific node IDs
204
205## Available Tools
206
207### URL Parsing
208- `parse_figma_url`: Parse any Figma URL to extract file key and node information
209
210### File Operations (require file key from parse_figma_url)
211- `get_file`: Get file structure using file key with depth control (default: 1)
212- `get_file_nodes`: Get specific nodes using file key with depth control (default: 1)
213- `export_images`: Export images from file using file key
214- `get_me`: Test authentication and get user info
215
216## Resources
217
218After exporting images using the `export_images` tool, they are available as MCP resources.
219You can:
220- List all exported images using the resources API
221- Access image data as base64-encoded blobs
222- Resources are identified by URIs like: `figma://file/{file_key}/node/{node_id}.{format}`
223
224## Depth Parameter
225
226Both `get_file` and `get_file_nodes` support a depth parameter to limit response size:
227
228- **depth=1** (default): For files: pages only. For nodes: direct children only
229- **depth=2**: For files: pages + top-level objects. For nodes: children + grandchildren
230- **depth=3+**: Deeper traversal (use carefully to avoid large responses)
231
232## Recursive Navigation Strategy
233
234To navigate large files without exceeding token limits:
235
2361. Start with `get_file` at depth=1 to see page structure
2372. Use `get_file_nodes` with specific page IDs at depth=1 to explore page contents
2383. Use `get_file_nodes` with specific component/frame IDs for deeper inspection
2394. Adjust depth as needed based on response size
240
241## Supported URL Formats
242- File: https://www.figma.com/file/FILE_ID/filename
243- File with node: https://www.figma.com/file/FILE_ID/filename?node-id=1%3A2
244- Design URL: https://www.figma.com/design/FILE_ID/filename
245
246## Authentication
247Set your Figma personal access token as an environment variable:
248export FIGMA_TOKEN="your_figma_token_here"
249
250Get your token from: https://www.figma.com/developers/api#access-tokens
251"#;
252
253 Ok(CallToolResult::success(vec![Content::text(
254 help_text.to_string(),
255 )]))
256 }
257}
258
259#[tool_handler]
260impl ServerHandler for FigmaServer {
261 fn get_info(&self) -> ServerInfo {
262 ServerInfo {
263 protocol_version: ProtocolVersion::V_2024_11_05,
264 server_info: Implementation::from_build_env(),
265 capabilities: ServerCapabilities::builder()
266 .enable_tools()
267 .enable_resources()
268 .build(),
269 instructions: Some("A Figma MCP server that provides tools to access Figma files and export images. Use 'help' tool for usage instructions.".into()),
270 }
271 }
272
273 fn list_resources(
274 &self,
275 _request: Option<PaginatedRequestParam>,
276 _context: RequestContext<RoleServer>,
277 ) -> impl Future<Output = Result<ListResourcesResult, McpError>> + Send + '_ {
278 async move {
279 let entries = self.image_cache.list_all()
280 .map_err(|e| McpError::internal_error(format!("Failed to list resources: {}", e), None))?;
281
282 let resources: Vec<Resource> = entries.iter().map(|(uri, entry)| {
283 let name = format!("Node {} Export", entry.node_id);
284 let description = format!(
285 "Exported from Figma file {} as {} ({}x scale)",
286 entry.file_key, entry.format, entry.scale
287 );
288 let mime_type = crate::figma::ImageCache::get_mime_type(&entry.format);
289
290 Resource::new(RawResource {
291 uri: uri.clone(),
292 name,
293 description: Some(description),
294 mime_type: Some(mime_type.to_string()),
295 size: entry.cached_data.as_ref().map(|data| data.len() as u32),
296 }, None)
297 }).collect();
298
299 Ok(ListResourcesResult { resources, next_cursor: None })
300 }
301 }
302
303 fn read_resource(
304 &self,
305 request: ReadResourceRequestParam,
306 _context: RequestContext<RoleServer>,
307 ) -> impl Future<Output = Result<ReadResourceResult, McpError>> + Send + '_ {
308 async move {
309 let uri = request.uri;
310
311 let entry = self.image_cache.get_entry(&uri)
312 .map_err(|e| McpError::internal_error(format!("Failed to get resource: {}", e), None))?
313 .ok_or_else(|| McpError::resource_not_found(format!("Resource not found: {}", uri), None))?;
314
315 let image_data = if let Some(cached_data) = entry.cached_data {
317 cached_data
318 } else {
319 if self.image_cache.is_expired(&entry) {
321 return Err(McpError::internal_error(
322 "Figma URL has expired. Please re-export the image.",
323 None
324 ));
325 }
326
327 let response = reqwest::get(&entry.figma_url).await
329 .map_err(|e| McpError::internal_error(format!("Failed to download image: {}", e), None))?;
330
331 if !response.status().is_success() {
332 return Err(McpError::internal_error(
333 format!("Failed to download image: HTTP {}", response.status()),
334 None
335 ));
336 }
337
338 let data = response.bytes().await
339 .map_err(|e| McpError::internal_error(format!("Failed to read image data: {}", e), None))?
340 .to_vec();
341
342 let _ = self.image_cache.update_cached_data(&uri, data.clone());
344
345 data
346 };
347
348 let base64_data = general_purpose::STANDARD.encode(&image_data);
350 let mime_type = crate::figma::ImageCache::get_mime_type(&entry.format);
351
352 Ok(ReadResourceResult {
353 contents: vec![ResourceContents::BlobResourceContents {
354 uri: uri.clone(),
355 mime_type: Some(mime_type.to_string()),
356 blob: base64_data,
357 }],
358 })
359 }
360 }
361}
362
363#[derive(Debug, Deserialize, schemars::JsonSchema)]
365struct ParseUrlRequest {
366 #[schemars(description = "The Figma URL to parse (file or design URL)")]
367 pub url: String,
368}
369
370#[derive(Debug, Deserialize, schemars::JsonSchema)]
371struct GetFileRequest {
372 #[schemars(description = "The Figma file key (extract from URL using parse_figma_url)")]
373 pub file_key: String,
374 #[schemars(
375 description = "Depth to traverse into the document tree (default: 1). Use 1 for pages only, 2 for pages + top-level objects, etc."
376 )]
377 pub depth: Option<u32>,
378}
379
380#[derive(Debug, Deserialize, schemars::JsonSchema)]
381struct ExportImageRequest {
382 #[schemars(description = "The Figma file key (extract from URL using parse_figma_url)")]
383 pub file_key: String,
384 #[schemars(description = "Comma-separated node IDs to export")]
385 pub node_ids: String,
386 #[schemars(description = "Export format: png, jpg, svg, OR pdf")]
387 pub format: Option<String>,
388 #[schemars(description = "Export scale factor (1.0, 2.0, 4.0)")]
389 pub scale: Option<f64>,
390}
391
392#[derive(Debug, Deserialize, schemars::JsonSchema)]
393struct GetFileNodesRequest {
394 #[schemars(description = "The Figma file key (extract from URL using parse_figma_url)")]
395 pub file_key: String,
396 #[schemars(description = "Comma-separated list of node IDs to fetch")]
397 pub node_ids: String,
398 #[schemars(
399 description = "Depth to traverse from each node (default: 1). Use 1 for direct children only, 2 for children + grandchildren, etc."
400 )]
401 pub depth: Option<u32>,
402}
403
404fn tool_error(message: String) -> Result<CallToolResult, McpError> {
406 Ok(CallToolResult::error(vec![Content::text(message)]))
407}
408
409fn tool_success(content: String) -> Result<CallToolResult, McpError> {
410 Ok(CallToolResult::success(vec![Content::text(content)]))
411}