figmanage 1.2.0 → 1.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/mcp.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { createServer } from 'node:http';
2
+ import { randomBytes } from 'node:crypto';
2
3
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
4
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
5
  import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
@@ -75,12 +76,24 @@ export async function startMcpServer() {
75
76
  registerTools(server, config, enabledToolsets, readOnly);
76
77
  const httpPort = parseHttpPort(process.argv);
77
78
  if (httpPort) {
79
+ // Bearer token auth for HTTP transport
80
+ let httpToken = process.env.FIGMA_HTTP_TOKEN || '';
81
+ if (!httpToken) {
82
+ httpToken = randomBytes(32).toString('hex');
83
+ console.error(`Generated HTTP bearer token: ${httpToken}`);
84
+ console.error('Set FIGMA_HTTP_TOKEN to use a fixed token.');
85
+ }
78
86
  const transport = new StreamableHTTPServerTransport({
79
87
  sessionIdGenerator: undefined,
80
88
  });
81
89
  const httpServer = createServer(async (req, res) => {
82
90
  const url = new URL(req.url ?? '/', `http://localhost:${httpPort}`);
83
91
  if (url.pathname === '/mcp') {
92
+ const auth = req.headers.authorization || '';
93
+ if (auth !== `Bearer ${httpToken}`) {
94
+ res.writeHead(401).end('Unauthorized');
95
+ return;
96
+ }
84
97
  await transport.handleRequest(req, res);
85
98
  }
86
99
  else {
@@ -459,13 +459,15 @@ export async function quarterlyDesignOpsReport(config, params) {
459
459
  params: { plan_parent_id: orgId, plan_type: 'organization' },
460
460
  }),
461
461
  ]);
462
- // Paginate all members (cursor-based, max 500)
462
+ // Paginate all members (cursor-based)
463
+ const errors = [];
463
464
  const allMembers = [];
464
465
  let cursor;
465
- const maxPages = 20;
466
+ let membersComplete = true;
467
+ const maxPages = 200; // safety cap: 200 * 50 = 10,000 members
466
468
  for (let page = 0; page < maxPages; page++) {
467
469
  try {
468
- const params = { page_size: 25 };
470
+ const params = { page_size: 50 };
469
471
  if (cursor)
470
472
  params.cursor = cursor;
471
473
  const res = await api.get(`/api/v2/orgs/${orgId}/org_users`, { params });
@@ -475,13 +477,19 @@ export async function quarterlyDesignOpsReport(config, params) {
475
477
  break;
476
478
  allMembers.push(...batch);
477
479
  cursor = Array.isArray(meta.cursor) ? meta.cursor[0] : meta.cursor;
478
- if (!cursor || batch.length < 25)
480
+ if (!cursor || batch.length < 50)
479
481
  break;
480
482
  }
481
- catch {
483
+ catch (e) {
484
+ membersComplete = false;
485
+ errors.push(`members: pagination stopped at page ${page + 1} of ${maxPages}, fetched ${allMembers.length} members (${e.response?.status || e.message})`);
482
486
  break;
483
487
  }
484
488
  }
489
+ if (cursor && allMembers.length >= maxPages * 50) {
490
+ membersComplete = false;
491
+ errors.push(`members: hit ${maxPages}-page safety cap at ${allMembers.length} members, org may have more`);
492
+ }
485
493
  // Process teams
486
494
  const teamsRaw = teamsResult.status === 'fulfilled'
487
495
  ? (teamsResult.value.data?.meta || teamsResult.value.data || [])
@@ -515,7 +523,6 @@ export async function quarterlyDesignOpsReport(config, params) {
515
523
  : 0;
516
524
  // Billing
517
525
  let billing = null;
518
- const errors = [];
519
526
  if (ratesResult.status === 'fulfilled') {
520
527
  const prices = ratesResult.value.data?.meta?.product_prices || [];
521
528
  const seatProducts = new Set(['expert', 'developer', 'collaborator']);
@@ -611,6 +618,7 @@ export async function quarterlyDesignOpsReport(config, params) {
611
618
  total_teams: teams.length,
612
619
  total_members: allMembers.length,
613
620
  total_paid_seats: totalPaid,
621
+ members_complete: membersComplete,
614
622
  },
615
623
  seat_utilization: {
616
624
  active_paid: activePaid,
@@ -47,17 +47,7 @@ export declare function seatOptimization(config: AuthConfig, params: {
47
47
  org_id?: string;
48
48
  days_inactive: number;
49
49
  include_cost: boolean;
50
- }): Promise<{
51
- summary: {
52
- total_paid: number;
53
- inactive_paid: number;
54
- monthly_waste_cents: any;
55
- annual_savings_cents: number;
56
- };
57
- seat_breakdown: any;
58
- inactive_users: any[];
59
- recommendations: string[];
60
- }>;
50
+ }): Promise<Record<string, any>>;
61
51
  export declare function permissionAudit(config: AuthConfig, params: {
62
52
  scope_type: 'project' | 'team';
63
53
  scope_id: string;
@@ -242,23 +242,36 @@ export async function seatOptimization(config, params) {
242
242
  const api = internalClient(config);
243
243
  const cutoff = Date.now() - days_inactive * 86400000;
244
244
  const paidKeys = new Set(['expert', 'developer', 'collaborator']);
245
- // Paginate org members (cursor-based, max 500)
245
+ // Paginate org members (cursor-based)
246
246
  const allMembers = [];
247
- const MAX_PAGES = 20;
247
+ const warnings = [];
248
+ let membersComplete = true;
249
+ const MAX_PAGES = 200; // safety cap: 200 * 50 = 10,000 members
248
250
  let cursor;
249
251
  for (let page = 0; page < MAX_PAGES; page++) {
250
- const params = { page_size: 25 };
251
- if (cursor)
252
- params.cursor = cursor;
253
- const res = await api.get(`/api/v2/orgs/${orgId}/org_users`, { params });
254
- const meta = res.data?.meta || {};
255
- const members = meta.users || [];
256
- if (!Array.isArray(members) || members.length === 0)
257
- break;
258
- allMembers.push(...members);
259
- cursor = Array.isArray(meta.cursor) ? meta.cursor[0] : meta.cursor;
260
- if (!cursor || members.length < 25)
252
+ try {
253
+ const params = { page_size: 50 };
254
+ if (cursor)
255
+ params.cursor = cursor;
256
+ const res = await api.get(`/api/v2/orgs/${orgId}/org_users`, { params });
257
+ const meta = res.data?.meta || {};
258
+ const members = meta.users || [];
259
+ if (!Array.isArray(members) || members.length === 0)
260
+ break;
261
+ allMembers.push(...members);
262
+ cursor = Array.isArray(meta.cursor) ? meta.cursor[0] : meta.cursor;
263
+ if (!cursor || members.length < 50)
264
+ break;
265
+ }
266
+ catch (e) {
267
+ membersComplete = false;
268
+ warnings.push(`members: pagination stopped at page ${page + 1}, fetched ${allMembers.length} members (${e.response?.status || e.message})`);
261
269
  break;
270
+ }
271
+ }
272
+ if (cursor && allMembers.length >= MAX_PAGES * 50) {
273
+ membersComplete = false;
274
+ warnings.push(`members: hit ${MAX_PAGES}-page safety cap at ${allMembers.length} members, org may have more`);
262
275
  }
263
276
  // Fetch seat breakdown and optionally contract rates in parallel
264
277
  const parallelCalls = [
@@ -317,17 +330,21 @@ export async function seatOptimization(config, params) {
317
330
  if (monthlyWasteCents > 0) {
318
331
  recommendations.push(`Potential monthly savings: $${(monthlyWasteCents / 100).toFixed(2)} ($${((monthlyWasteCents * 12) / 100).toFixed(2)}/yr).`);
319
332
  }
320
- return {
333
+ const result = {
321
334
  summary: {
322
335
  total_paid: totalPaid,
323
336
  inactive_paid: inactiveUsers.length,
324
337
  monthly_waste_cents: monthlyWasteCents,
325
338
  annual_savings_cents: monthlyWasteCents * 12,
339
+ members_complete: membersComplete,
326
340
  },
327
341
  seat_breakdown: seats,
328
342
  inactive_users: inactiveUsers,
329
343
  recommendations,
330
344
  };
345
+ if (warnings.length > 0)
346
+ result.warnings = warnings;
347
+ return result;
331
348
  }
332
349
  // -- permission_audit --
333
350
  export async function permissionAudit(config, params) {
package/dist/setup.js CHANGED
@@ -1,279 +1,14 @@
1
1
  #!/usr/bin/env node
2
- import { execSync, execFileSync } from 'child_process';
3
- import { createDecipheriv, pbkdf2Sync } from 'crypto';
4
- import { copyFileSync, unlinkSync, mkdtempSync, mkdirSync, existsSync, rmdirSync, readFileSync, writeFileSync } from 'fs';
2
+ import { execFileSync } from 'child_process';
3
+ import { mkdirSync, existsSync, readFileSync, writeFileSync } from 'fs';
5
4
  import { join } from 'path';
6
- import { tmpdir, homedir, platform } from 'os';
7
- import axios from 'axios';
8
- const COOKIE_NAME = '__Host-figma.authn';
9
- // --- Platform-specific Chrome paths ---
10
- function getChromePaths() {
11
- switch (platform()) {
12
- case 'darwin':
13
- return [join(homedir(), 'Library/Application Support/Google/Chrome')];
14
- case 'linux':
15
- return [
16
- join(homedir(), '.config/google-chrome'),
17
- join(homedir(), '.config/chromium'),
18
- ];
19
- case 'win32': {
20
- const localAppData = process.env.LOCALAPPDATA || join(homedir(), 'AppData/Local');
21
- return [join(localAppData, 'Google/Chrome/User Data')];
22
- }
23
- default:
24
- return [];
25
- }
26
- }
27
- // --- Chrome profile discovery ---
28
- function findChromeProfiles() {
29
- const chromePaths = getChromePaths();
30
- const profiles = [];
31
- for (const base of chromePaths) {
32
- if (!existsSync(base))
33
- continue;
34
- const defaultProfile = join(base, 'Default');
35
- if (existsSync(join(defaultProfile, 'Cookies')))
36
- profiles.push(defaultProfile);
37
- for (let i = 1; i <= 20; i++) {
38
- const profile = join(base, `Profile ${i}`);
39
- if (existsSync(join(profile, 'Cookies')))
40
- profiles.push(profile);
41
- }
42
- }
43
- if (profiles.length === 0)
44
- throw new Error('No Chrome profiles with Cookies found.');
45
- return profiles;
46
- }
47
- // --- macOS cookie decryption ---
48
- function getMacDecryptionKey() {
49
- const password = execFileSync('security', ['find-generic-password', '-w', '-s', 'Chrome Safe Storage'], { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
50
- return pbkdf2Sync(password, 'saltysalt', 1003, 16, 'sha1');
51
- }
52
- // --- Linux cookie decryption ---
53
- function getLinuxDecryptionKey() {
54
- // Try GNOME Keyring first via secret-tool
55
- try {
56
- const password = execSync('secret-tool lookup application chrome', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
57
- if (password)
58
- return pbkdf2Sync(password, 'saltysalt', 1, 16, 'sha1');
59
- }
60
- catch {
61
- // secret-tool not available or no entry
62
- }
63
- // Fall back to default Chrome password
64
- return pbkdf2Sync('peanuts', 'saltysalt', 1, 16, 'sha1');
65
- }
66
- // --- Windows cookie decryption ---
67
- function getWindowsDecryptionKey(chromeBase) {
68
- const localStatePath = join(chromeBase, 'Local State');
69
- if (!existsSync(localStatePath)) {
70
- throw new Error('Chrome Local State file not found. Cannot decrypt cookies on Windows.');
71
- }
72
- const localState = JSON.parse(readFileSync(localStatePath, 'utf-8'));
73
- const encryptedKeyB64 = localState?.os_crypt?.encrypted_key;
74
- if (!encryptedKeyB64) {
75
- throw new Error('No encrypted_key in Chrome Local State.');
76
- }
77
- // The key is base64-encoded, with a 'DPAPI' prefix (5 bytes) before the actual DPAPI blob
78
- const encryptedKey = Buffer.from(encryptedKeyB64, 'base64');
79
- if (encryptedKey.toString('utf-8', 0, 5) !== 'DPAPI') {
80
- throw new Error('Unexpected encrypted_key format (missing DPAPI prefix).');
81
- }
82
- const dpapiBlob = encryptedKey.slice(5).toString('base64');
83
- // Use PowerShell to call DPAPI Unprotect
84
- const psScript = `
85
- Add-Type -AssemblyName System.Security
86
- $blob = [Convert]::FromBase64String('${dpapiBlob}')
87
- $dec = [Security.Cryptography.ProtectedData]::Unprotect($blob, $null, [Security.Cryptography.DataProtectionScope]::CurrentUser)
88
- [Convert]::ToBase64String($dec)
89
- `.trim().replace(/\n/g, '; ');
90
- const decryptedB64 = execSync(`powershell -NoProfile -NonInteractive -Command "${psScript}"`, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
91
- return Buffer.from(decryptedB64, 'base64');
92
- }
93
- // --- Decryption ---
94
- function decryptCBC(encrypted, key) {
95
- // v10 prefix = Chrome AES-128-CBC encryption (macOS and Linux)
96
- if (encrypted.length > 3 && encrypted[0] === 0x76 && encrypted[1] === 0x31 && encrypted[2] === 0x30) {
97
- const iv = Buffer.alloc(16, 0x20); // Chrome uses space (0x20) as IV
98
- const decipher = createDecipheriv('aes-128-cbc', key, iv);
99
- const decrypted = Buffer.concat([decipher.update(encrypted.slice(3)), decipher.final()]);
100
- // Chrome may prepend binary metadata before the cookie value.
101
- // The actual cookie is URL-encoded JSON starting with %7B or raw JSON starting with {
102
- const str = decrypted.toString('binary');
103
- const jsonStart = str.indexOf('%7B');
104
- if (jsonStart >= 0)
105
- return str.slice(jsonStart);
106
- const rawJsonStart = str.indexOf('{');
107
- if (rawJsonStart >= 0)
108
- return str.slice(rawJsonStart);
109
- throw new Error('Decrypted cookie data does not contain expected JSON value');
110
- }
111
- return encrypted.toString('utf-8');
112
- }
113
- function decryptWindows(encrypted, key) {
114
- // Windows Chrome uses AES-256-GCM with v10 prefix
115
- // Format: v10 (3 bytes) + nonce (12 bytes) + ciphertext + tag (16 bytes)
116
- if (encrypted.length > 3 && encrypted[0] === 0x76 && encrypted[1] === 0x31 && encrypted[2] === 0x30) {
117
- const nonce = encrypted.slice(3, 15);
118
- const ciphertextWithTag = encrypted.slice(15);
119
- const tag = ciphertextWithTag.slice(ciphertextWithTag.length - 16);
120
- const ciphertext = ciphertextWithTag.slice(0, ciphertextWithTag.length - 16);
121
- const decipher = createDecipheriv('aes-256-gcm', key, nonce);
122
- decipher.setAuthTag(tag);
123
- const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
124
- const str = decrypted.toString('utf-8');
125
- const jsonStart = str.indexOf('%7B');
126
- if (jsonStart >= 0)
127
- return str.slice(jsonStart);
128
- const rawJsonStart = str.indexOf('{');
129
- if (rawJsonStart >= 0)
130
- return str.slice(rawJsonStart);
131
- throw new Error('Decrypted cookie data does not contain expected JSON value');
132
- }
133
- return encrypted.toString('utf-8');
134
- }
135
- // --- Cookie extraction ---
136
- function extractCookie(profilePath) {
137
- const cookiesDb = join(profilePath, 'Cookies');
138
- const tmpDir = mkdtempSync(join(tmpdir(), 'figmanage-'));
139
- const tmpDb = join(tmpDir, 'Cookies');
140
- // Copy DB + WAL/SHM (Chrome locks the original)
141
- copyFileSync(cookiesDb, tmpDb);
142
- for (const ext of ['-wal', '-shm']) {
143
- const src = cookiesDb + ext;
144
- if (existsSync(src))
145
- copyFileSync(src, tmpDb + ext);
146
- }
147
- try {
148
- // Query for the auth cookie -- could be on figma.com or www.figma.com
149
- const sqliteBin = platform() === 'win32' ? 'sqlite3.exe' : 'sqlite3';
150
- const hex = execSync(`${sqliteBin} "${tmpDb}" "SELECT hex(encrypted_value) FROM cookies WHERE name = '${COOKIE_NAME}' AND host_key LIKE '%figma.com' ORDER BY last_access_utc DESC LIMIT 1;"`, { encoding: 'utf-8' }).trim();
151
- if (!hex)
152
- throw new Error(`No ${COOKIE_NAME} cookie found. Are you logged into figma.com in Chrome?`);
153
- const encrypted = Buffer.from(hex, 'hex');
154
- const os = platform();
155
- if (os === 'darwin') {
156
- const key = getMacDecryptionKey();
157
- return decryptCBC(encrypted, key);
158
- }
159
- else if (os === 'linux') {
160
- const key = getLinuxDecryptionKey();
161
- return decryptCBC(encrypted, key);
162
- }
163
- else if (os === 'win32') {
164
- // Derive the chrome base from the profile path (go up one level from Default/Profile N)
165
- const chromeBase = join(profilePath, '..');
166
- const key = getWindowsDecryptionKey(chromeBase);
167
- return decryptWindows(encrypted, key);
168
- }
169
- throw new Error(`Unsupported platform: ${os}`);
170
- }
171
- finally {
172
- for (const f of [tmpDb, tmpDb + '-wal', tmpDb + '-shm']) {
173
- try {
174
- unlinkSync(f);
175
- }
176
- catch { }
177
- }
178
- try {
179
- rmdirSync(tmpDir);
180
- }
181
- catch { }
182
- }
183
- }
184
- // --- Figma API validation ---
185
- function parseCookieValue(raw) {
186
- // Cookie value is JSON: {"userId":"token"} (may be URL-encoded)
187
- let decoded = raw;
188
- try {
189
- decoded = decodeURIComponent(raw);
190
- }
191
- catch { }
192
- try {
193
- const parsed = JSON.parse(decoded);
194
- const entries = Object.entries(parsed);
195
- if (entries.length === 0)
196
- throw new Error('Empty cookie JSON');
197
- const [userId, token] = entries[0];
198
- return { userId, token, cookieValue: raw };
199
- }
200
- catch {
201
- throw new Error('Unexpected cookie format. Expected URL-encoded JSON with userId field.');
202
- }
203
- }
204
- async function validateSession(cookieValue, userId) {
205
- const headers = {
206
- 'Cookie': `${COOKIE_NAME}=${cookieValue}`,
207
- 'X-CSRF-Bypass': 'yes',
208
- 'X-Figma-User-Id': userId,
209
- };
210
- const res = await axios.get('https://www.figma.com/api/user/state', {
211
- headers,
212
- timeout: 15000,
213
- });
214
- if (res.data?.error !== false)
215
- throw new Error('Session invalid');
216
- const meta = res.data.meta || {};
217
- const teams = (meta.teams || []).map((t) => ({ id: String(t.id), name: t.name }));
218
- const orgs = (meta.orgs || []).map((o) => ({ id: String(o.id), name: o.name }));
219
- // Try to find org_id: check orgs array, team_users, or follow the recents redirect
220
- let orgId = '';
221
- if (orgs.length > 0) {
222
- orgId = orgs[0].id;
223
- }
224
- else {
225
- // Figma redirects /files/recents-and-sharing to /files/{org_id}/recents-and-sharing
226
- try {
227
- const redirect = await axios.get('https://www.figma.com/files/recents-and-sharing', {
228
- headers,
229
- maxRedirects: 0,
230
- validateStatus: (s) => s >= 200 && s < 400,
231
- timeout: 10000,
232
- });
233
- // Check final URL for org_id pattern: /files/{org_id}/
234
- const finalUrl = redirect.request?.res?.responseUrl || redirect.headers?.location || '';
235
- const match = finalUrl.match(/\/files\/(\d+)\//);
236
- if (match)
237
- orgId = match[1];
238
- }
239
- catch (e) {
240
- // Check redirect location header
241
- const loc = e.response?.headers?.location || '';
242
- const match = loc.match(/\/files\/(\d+)\//);
243
- if (match)
244
- orgId = match[1];
245
- }
246
- }
247
- // If orgId found but no orgs entry, try to derive a name from the org's domain
248
- if (orgId && orgs.length === 0) {
249
- let name = orgId;
250
- try {
251
- const domRes = await axios.get(`https://www.figma.com/api/orgs/${orgId}/domains`, {
252
- headers,
253
- timeout: 10000,
254
- });
255
- const domains = domRes.data?.meta || [];
256
- if (Array.isArray(domains) && domains.length > 0 && domains[0].domain) {
257
- name = domains[0].domain;
258
- }
259
- }
260
- catch { /* domain lookup optional */ }
261
- orgs.push({ id: orgId, name });
262
- }
263
- return { orgId, orgs, teams };
264
- }
265
- // --- PAT validation ---
266
- async function validatePat(pat) {
267
- const res = await axios.get('https://api.figma.com/v1/me', {
268
- headers: { 'X-Figma-Token': pat },
269
- timeout: 15000,
270
- });
271
- return res.data.handle || res.data.email || 'valid';
272
- }
5
+ import { homedir, platform } from 'os';
6
+ import { extractCookies, validateSession, validatePat } from './auth/cookie.js';
273
7
  // --- MCP client detection and registration ---
274
8
  function claudeCliAvailable() {
275
9
  try {
276
- execSync('which claude 2>/dev/null || where claude 2>nul', {
10
+ const whichCmd = platform() === 'win32' ? 'where' : 'which';
11
+ execFileSync(whichCmd, ['claude'], {
277
12
  encoding: 'utf-8',
278
13
  stdio: ['pipe', 'pipe', 'pipe'],
279
14
  });
@@ -285,9 +20,20 @@ function claudeCliAvailable() {
285
20
  }
286
21
  function registerWithClaude(envVars) {
287
22
  try {
288
- execSync('claude mcp remove figmanage -s user 2>/dev/null || true', { encoding: 'utf-8' });
289
- const envFlags = Object.entries(envVars).map(([k, v]) => `--env ${k}=${v}`).join(' ');
290
- execSync(`claude mcp add figmanage --transport stdio -s user ${envFlags} -- npx -y figmanage`, { encoding: 'utf-8' });
23
+ try {
24
+ execFileSync('claude', ['mcp', 'remove', 'figmanage', '-s', 'user'], {
25
+ encoding: 'utf-8',
26
+ stdio: ['pipe', 'pipe', 'pipe'],
27
+ });
28
+ }
29
+ catch {
30
+ // Ignore if not previously registered
31
+ }
32
+ execFileSync('claude', [
33
+ 'mcp', 'add', 'figmanage', '--transport', 'stdio', '-s', 'user',
34
+ ...Object.entries(envVars).flatMap(([k, v]) => ['--env', `${k}=${v}`]),
35
+ '--', 'npx', '-y', 'figmanage',
36
+ ], { encoding: 'utf-8' });
291
37
  return true;
292
38
  }
293
39
  catch {
@@ -414,17 +160,7 @@ async function setup() {
414
160
  console.log(`Reading Chrome cookies${promptLabel}...`);
415
161
  let accounts = [];
416
162
  try {
417
- const profiles = findChromeProfiles();
418
- for (const profilePath of profiles) {
419
- try {
420
- const rawCookie = extractCookie(profilePath);
421
- const { userId, cookieValue } = parseCookieValue(rawCookie);
422
- accounts.push({ userId, cookieValue, profile: profilePath.split(/[/\\]/).pop() });
423
- }
424
- catch {
425
- // Profile doesn't have a Figma cookie
426
- }
427
- }
163
+ accounts = extractCookies();
428
164
  }
429
165
  catch (e) {
430
166
  if (os === 'win32') {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "figmanage",
3
3
  "mcpName": "io.github.dannykeane/figmanage",
4
- "version": "1.2.0",
4
+ "version": "1.2.3",
5
5
  "description": "MCP server for managing your Figma workspace from the terminal.",
6
6
  "type": "module",
7
7
  "main": "dist/index.js",
@@ -40,8 +40,8 @@
40
40
  },
41
41
  "homepage": "https://github.com/dannykeane/figmanage#readme",
42
42
  "dependencies": {
43
- "@modelcontextprotocol/sdk": "^1.25.0",
44
- "axios": "^1.7.0",
43
+ "@modelcontextprotocol/sdk": "^1.25.2",
44
+ "axios": "^1.13.5",
45
45
  "axios-retry": "^4.4.0",
46
46
  "commander": "^14.0.3",
47
47
  "zod": "^3.23.0"