figma_mcp/figma/
client.rs1use reqwest::{header::HeaderMap, header::HeaderValue, Client};
2use serde_json::Value;
3
4use crate::{Error, Result};
5
6const FIGMA_API_BASE: &str = "https://api.figma.com/v1";
7
8#[derive(Debug, Clone)]
9pub struct FigmaClient {
10 client: Client,
11 token: String,
12}
13
14impl FigmaClient {
15 pub fn new(token: String) -> Result<Self> {
16 let mut headers = HeaderMap::new();
17 headers.insert(
18 "X-Figma-Token",
19 HeaderValue::from_str(&token)
20 .map_err(|_| Error::Auth("Invalid token format".to_string()))?,
21 );
22
23 let client = Client::builder()
24 .default_headers(headers)
25 .build()
26 .map_err(|e| Error::Network(e))?;
27
28 Ok(Self { client, token })
29 }
30
31 pub async fn get_file(&self, file_id: &str, depth: Option<u32>) -> Result<Value> {
32 let mut url = format!("{}/files/{}", FIGMA_API_BASE, file_id);
33 if let Some(depth) = depth {
34 url.push_str(&format!("?depth={}", depth));
35 }
36 let response = self.client.get(&url).send().await?;
37
38 if !response.status().is_success() {
39 let status = response.status();
40 let text = response.text().await.unwrap_or_default();
41 return Err(Error::FigmaApi(format!("HTTP {}: {}", status, text)));
42 }
43
44 let json: Value = response.json().await?;
45
46 if let Some(err) = json.get("err") {
47 if !err.is_null() {
48 return Err(Error::FigmaApi(err.to_string()));
49 }
50 }
51
52 Ok(json)
53 }
54
55 pub async fn get_file_nodes(
56 &self,
57 file_id: &str,
58 node_ids: &[String],
59 depth: Option<u32>,
60 ) -> Result<Value> {
61 let ids = node_ids.join(",");
62 let mut url = format!("{}/files/{}/nodes?ids={}", FIGMA_API_BASE, file_id, ids);
63 if let Some(depth) = depth {
64 url.push_str(&format!("&depth={}", depth));
65 }
66 let response = self.client.get(&url).send().await?;
67
68 if !response.status().is_success() {
69 let status = response.status();
70 let text = response.text().await.unwrap_or_default();
71 return Err(Error::FigmaApi(format!("HTTP {}: {}", status, text)));
72 }
73
74 let json: Value = response.json().await?;
75
76 if let Some(err) = json.get("err") {
77 if !err.is_null() {
78 return Err(Error::FigmaApi(err.to_string()));
79 }
80 }
81
82 Ok(json)
83 }
84
85 pub async fn export_images(
86 &self,
87 file_id: &str,
88 node_ids: &[String],
89 format: &str,
90 scale: Option<f64>,
91 ) -> Result<Value> {
92 let ids = node_ids.join(",");
93 let mut url = format!(
94 "{}/images/{}?ids={}&format={}",
95 FIGMA_API_BASE, file_id, ids, format
96 );
97
98 if let Some(scale) = scale {
99 url.push_str(&format!("&scale={}", scale));
100 }
101
102 let response = self.client.get(&url).send().await?;
103
104 if !response.status().is_success() {
105 let status = response.status();
106 let text = response.text().await.unwrap_or_default();
107 return Err(Error::FigmaApi(format!("HTTP {}: {}", status, text)));
108 }
109
110 let json: Value = response.json().await?;
111
112 if let Some(err) = json.get("err") {
113 if !err.is_null() {
114 return Err(Error::FigmaApi(err.to_string()));
115 }
116 }
117
118 Ok(json)
119 }
120
121 pub async fn get_me(&self) -> Result<Value> {
122 let url = format!("{}/me", FIGMA_API_BASE);
123 let response = self.client.get(&url).send().await?;
124
125 if !response.status().is_success() {
126 let status = response.status();
127 let text = response.text().await.unwrap_or_default();
128 return Err(Error::FigmaApi(format!("HTTP {}: {}", status, text)));
129 }
130
131 let json: Value = response.json().await?;
132
133 if let Some(err) = json.get("err") {
134 if !err.is_null() {
135 return Err(Error::FigmaApi(err.to_string()));
136 }
137 }
138
139 Ok(json)
140 }
141
142 pub fn get_token(&self) -> &str {
143 &self.token
144 }
145}
146
147#[cfg(test)]
148mod tests {
149 use super::*;
150
151 #[tokio::test]
152 async fn test_client_creation() {
153 let client = FigmaClient::new("test-token".to_string());
154 assert!(client.is_ok());
155 }
156
157 #[tokio::test]
158 async fn test_invalid_token_format() {
159 let client = FigmaClient::new("invalid\ntoken".to_string());
160 assert!(client.is_err());
161 }
162}