figmanage 0.1.2 → 0.2.1

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/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  MCP server for managing your Figma workspace.
4
4
 
5
- Manages Figma workspaces through AI assistants. 76 tools covering files, projects, teams, permissions, comments, versions, components, webhooks, org admin, and more. Works with Claude Code, Claude Desktop, ChatGPT, and any MCP-compatible client.
5
+ Manages Figma workspaces through AI assistants. 79 tools covering files, projects, teams, permissions, comments, versions, components, webhooks, org admin, and more. Works with Claude Code, Claude Desktop, ChatGPT, and any MCP-compatible client.
6
6
 
7
7
  ## quick start
8
8
 
@@ -16,19 +16,25 @@ claude mcp add figmanage -s user -e FIGMA_PAT=figd_xxx -- npx -y figmanage
16
16
 
17
17
  Restart Claude Code. Gives you 30+ tools (comments, reading, export, components, versions, webhooks).
18
18
 
19
- ### full setup (2 minutes, all 76 tools)
19
+ ### full setup (2 minutes, all 79 tools)
20
20
 
21
21
  ```bash
22
22
  npx -y figmanage --setup
23
23
  ```
24
24
 
25
- Extracts your Chrome cookie, prompts for a PAT, registers with Claude Code automatically. Unlocks all 76 tools including workspace management, permissions, and org admin. Restart Claude Code.
25
+ Extracts your Chrome cookie, prompts for a PAT, registers with Claude Code automatically. Unlocks all 79 tools including workspace management, permissions, and org admin. Restart Claude Code.
26
26
 
27
27
  Use `--no-prompt --pat figd_xxx` for non-interactive setup.
28
28
 
29
- ### Claude Desktop
29
+ ### Claude Desktop / Cowork
30
30
 
31
- Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
31
+ Full setup with cookie extraction:
32
+
33
+ ```bash
34
+ npx -y figmanage --setup --desktop
35
+ ```
36
+
37
+ Or manually add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
32
38
 
33
39
  ```json
34
40
  {
@@ -223,7 +229,7 @@ Tools are automatically filtered at startup based on available credentials. If y
223
229
  | `library_usage` | cookie | Team-level library adoption metrics over a lookback period |
224
230
  | `component_usage` | cookie | Per-file component usage analytics |
225
231
 
226
- ### org (9 tools)
232
+ ### org (12 tools)
227
233
 
228
234
  | Tool | Auth | Description |
229
235
  |------|------|-------------|
@@ -231,6 +237,9 @@ Tools are automatically filtered at startup based on available credentials. If y
231
237
  | `list_org_teams` | cookie | All teams with member counts, project counts, access levels |
232
238
  | `seat_usage` | cookie | Seat breakdown: permissions, seat types, activity, account types |
233
239
  | `list_team_members` | cookie | Team members with name, email, role, last active date |
240
+ | `list_org_members` | cookie | List org members with seat type, permission, email, last active |
241
+ | `contract_rates` | cookie | Seat pricing per product (expert, developer, collaborator) |
242
+ | `change_seat` | cookie | Change a user's seat type (confirm flag required for upgrades) |
234
243
  | `billing_overview` | cookie | Invoice history, billing status, amounts, billing periods |
235
244
  | `list_invoices` | cookie | Open and upcoming invoices |
236
245
  | `org_domains` | cookie | Domain configuration and SSO/SAML settings |
package/dist/setup.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { execSync, execFileSync } from 'child_process';
3
3
  import { createDecipheriv, pbkdf2Sync } from 'crypto';
4
- import { copyFileSync, unlinkSync, mkdtempSync, existsSync, rmdirSync, readFileSync } from 'fs';
4
+ import { copyFileSync, unlinkSync, mkdtempSync, mkdirSync, existsSync, rmdirSync, readFileSync, writeFileSync } from 'fs';
5
5
  import { join } from 'path';
6
6
  import { tmpdir, homedir, platform } from 'os';
7
7
  import axios from 'axios';
@@ -294,8 +294,32 @@ function registerWithClaude(envVars) {
294
294
  return false;
295
295
  }
296
296
  }
297
+ function registerWithDesktop(envVars) {
298
+ const configPath = platform() === 'win32'
299
+ ? join(process.env.APPDATA || join(homedir(), 'AppData/Roaming'), 'Claude/claude_desktop_config.json')
300
+ : join(homedir(), 'Library/Application Support/Claude/claude_desktop_config.json');
301
+ try {
302
+ let config = {};
303
+ if (existsSync(configPath)) {
304
+ config = JSON.parse(readFileSync(configPath, 'utf-8'));
305
+ }
306
+ if (!config.mcpServers)
307
+ config.mcpServers = {};
308
+ config.mcpServers.figmanage = {
309
+ command: 'npx',
310
+ args: ['-y', 'figmanage'],
311
+ env: envVars,
312
+ };
313
+ mkdirSync(join(configPath, '..'), { recursive: true });
314
+ writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
315
+ return true;
316
+ }
317
+ catch {
318
+ return false;
319
+ }
320
+ }
297
321
  function printManualConfig(envVars) {
298
- console.log('\nClaude CLI not found. Configure your MCP client manually.\n');
322
+ console.log('\nConfigure your MCP client manually.\n');
299
323
  console.log('Environment variables:');
300
324
  for (const [k, v] of Object.entries(envVars)) {
301
325
  const display = (k === 'FIGMA_AUTH_COOKIE' || k === 'FIGMA_PAT') ? '******' : v;
@@ -315,21 +339,25 @@ function printManualConfig(envVars) {
315
339
  function parseArgs() {
316
340
  const args = process.argv.slice(2);
317
341
  let noPrompt = false;
342
+ let desktop = false;
318
343
  let pat;
319
344
  for (let i = 0; i < args.length; i++) {
320
345
  if (args[i] === '--no-prompt') {
321
346
  noPrompt = true;
322
347
  }
348
+ else if (args[i] === '--desktop') {
349
+ desktop = true;
350
+ }
323
351
  else if (args[i] === '--pat' && i + 1 < args.length) {
324
352
  pat = args[++i];
325
353
  }
326
354
  }
327
- return { noPrompt, pat };
355
+ return { noPrompt, desktop, pat };
328
356
  }
329
357
  // --- Main ---
330
358
  async function setup() {
331
359
  console.log('figmanage setup\n');
332
- const { noPrompt, pat: patArg } = parseArgs();
360
+ const { noPrompt, desktop, pat: patArg } = parseArgs();
333
361
  const os = platform();
334
362
  // Build env vars to register
335
363
  const envVars = {};
@@ -351,8 +379,18 @@ async function setup() {
351
379
  process.exit(1);
352
380
  }
353
381
  // Register with whatever client is available
354
- if (claudeCliAvailable()) {
355
- console.log('\nRegistering with Claude...');
382
+ if (desktop) {
383
+ console.log('\nRegistering with Claude Desktop...');
384
+ if (registerWithDesktop(envVars)) {
385
+ console.log(' Credentials written to claude_desktop_config.json');
386
+ console.log(' Done. Restart Claude Desktop to use figmanage.');
387
+ }
388
+ else {
389
+ printManualConfig(envVars);
390
+ }
391
+ }
392
+ else if (claudeCliAvailable()) {
393
+ console.log('\nRegistering with Claude Code...');
356
394
  if (registerWithClaude(envVars)) {
357
395
  console.log(' PAT stored in MCP server config');
358
396
  console.log(' Done. Restart Claude Code to use figmanage.');
@@ -542,8 +580,19 @@ async function setup() {
542
580
  console.log(`FIGMA_ORG_ID=${orgId}`);
543
581
  if (pat)
544
582
  console.log('FIGMA_PAT=****** (stored in MCP server config)');
545
- if (claudeCliAvailable()) {
546
- console.log('\nRegistering with Claude...');
583
+ if (desktop) {
584
+ console.log('\nRegistering with Claude Desktop...');
585
+ if (registerWithDesktop(envVars)) {
586
+ console.log(' Credentials written to claude_desktop_config.json');
587
+ console.log(' Done. Restart Claude Desktop to use figmanage.');
588
+ }
589
+ else {
590
+ console.log(' Could not write config automatically.');
591
+ printManualConfig(envVars);
592
+ }
593
+ }
594
+ else if (claudeCliAvailable()) {
595
+ console.log('\nRegistering with Claude Code...');
547
596
  if (registerWithClaude(envVars)) {
548
597
  if (pat)
549
598
  console.log(' PAT stored in MCP server config');
package/dist/tools/org.js CHANGED
@@ -308,4 +308,175 @@ defineTool({
308
308
  });
309
309
  },
310
310
  });
311
+ // -- list_org_members --
312
+ defineTool({
313
+ toolset: 'org',
314
+ auth: 'cookie',
315
+ register(server, config) {
316
+ server.registerTool('list_org_members', {
317
+ description: 'List org members with seat type, permission, email, and last active date. Use to resolve org_user_ids for change_seat.',
318
+ inputSchema: {
319
+ org_id: figmaId.optional().describe('Org ID override (defaults to current workspace)'),
320
+ search_query: z.string().optional().describe('Filter members by name or email'),
321
+ },
322
+ }, async ({ org_id, search_query }) => {
323
+ try {
324
+ let orgId;
325
+ try {
326
+ orgId = requireOrgId(config, org_id);
327
+ }
328
+ catch (e) {
329
+ return toolError(e.message);
330
+ }
331
+ const params = {};
332
+ if (search_query)
333
+ params.search_query = search_query;
334
+ const res = await internalClient(config).get(`/api/v2/orgs/${orgId}/org_users`, { params });
335
+ const members = (res.data?.meta || res.data || []).map((m) => ({
336
+ org_user_id: String(m.id),
337
+ user_id: m.user_id,
338
+ email: m.user?.email,
339
+ name: m.user?.handle,
340
+ permission: m.permission,
341
+ seat_type: m.active_seat_type?.key || null,
342
+ last_active: m.last_active,
343
+ }));
344
+ return toolResult(JSON.stringify(members, null, 2));
345
+ }
346
+ catch (e) {
347
+ return toolError(`Failed to list org members: ${e.response?.status || e.message}`);
348
+ }
349
+ });
350
+ },
351
+ });
352
+ // -- contract_rates --
353
+ defineTool({
354
+ toolset: 'org',
355
+ auth: 'cookie',
356
+ register(server, config) {
357
+ server.registerTool('contract_rates', {
358
+ description: 'Seat pricing for the org. Returns monthly cost per seat type (expert, developer, collaborator).',
359
+ inputSchema: {
360
+ org_id: figmaId.optional().describe('Org ID override (defaults to current workspace)'),
361
+ },
362
+ }, async ({ org_id }) => {
363
+ try {
364
+ let orgId;
365
+ try {
366
+ orgId = requireOrgId(config, org_id);
367
+ }
368
+ catch (e) {
369
+ return toolError(e.message);
370
+ }
371
+ const res = await internalClient(config).get(`/api/pricing/contract_rates`, { params: { plan_parent_id: orgId, plan_type: 'organization' } });
372
+ const seatProducts = new Set(['expert', 'developer', 'collaborator']);
373
+ const prices = res.data?.meta?.product_prices || [];
374
+ const rates = prices
375
+ .filter((r) => seatProducts.has(r.billable_product_key))
376
+ .map((r) => ({
377
+ product: r.billable_product_key,
378
+ monthly_cents: r.amount,
379
+ monthly_dollars: (r.amount / 100).toFixed(2),
380
+ }));
381
+ return toolResult(JSON.stringify(rates, null, 2));
382
+ }
383
+ catch (e) {
384
+ return toolError(`Failed to fetch contract rates: ${e.response?.status || e.message}`);
385
+ }
386
+ });
387
+ },
388
+ });
389
+ // -- change_seat --
390
+ const SEAT_HIERARCHY = {
391
+ view: 0,
392
+ collab: 1,
393
+ dev: 2,
394
+ full: 3,
395
+ };
396
+ const SEAT_LABELS = {
397
+ view: 'Viewer (free)',
398
+ collab: 'Collaborator ($5/mo)',
399
+ dev: 'Developer ($25/mo)',
400
+ full: 'Full ($55/mo)',
401
+ };
402
+ const PAID_STATUSES = {
403
+ full: { expert: 'full' },
404
+ dev: { developer: 'full' },
405
+ collab: { collaborator: 'full' },
406
+ view: { collaborator: 'starter', developer: 'starter', expert: 'starter' },
407
+ };
408
+ const SEAT_KEY_TO_TYPE = {
409
+ expert: 'full',
410
+ developer: 'dev',
411
+ collaborator: 'collab',
412
+ };
413
+ defineTool({
414
+ toolset: 'org',
415
+ auth: 'cookie',
416
+ mutates: true,
417
+ destructive: true,
418
+ register(server, config) {
419
+ server.registerTool('change_seat', {
420
+ description: 'Change a user\'s seat type. Accepts user_id or email to identify the user. Upgrades affect billing.',
421
+ inputSchema: {
422
+ user_id: z.string().describe('User ID or email address of the target user'),
423
+ seat_type: z.enum(['full', 'dev', 'collab', 'view']).describe('Target seat type'),
424
+ org_id: figmaId.optional().describe('Org ID override (defaults to current workspace)'),
425
+ confirm: z.boolean().optional().describe('Required when upgrading to a higher/paid seat. Set to true to authorize the billing change.'),
426
+ },
427
+ }, async ({ user_id, seat_type, org_id, confirm }) => {
428
+ try {
429
+ let orgId;
430
+ try {
431
+ orgId = requireOrgId(config, org_id);
432
+ }
433
+ catch (e) {
434
+ return toolError(e.message);
435
+ }
436
+ const res = await internalClient(config).get(`/api/v2/orgs/${orgId}/org_users`, {
437
+ params: user_id.includes('@') ? { search_query: user_id } : {},
438
+ });
439
+ const members = res.data?.meta || res.data || [];
440
+ const member = members.find((m) => user_id.includes('@')
441
+ ? m.user?.email === user_id
442
+ : String(m.user_id) === String(user_id));
443
+ if (!member)
444
+ return toolError(`User not found: ${user_id}`);
445
+ if (String(member.user_id) === String(config.userId)) {
446
+ return toolError('Cannot change your own seat type. Use the Figma admin panel.');
447
+ }
448
+ const currentKey = member.active_seat_type?.key || null;
449
+ const currentType = currentKey ? (SEAT_KEY_TO_TYPE[currentKey] || 'view') : 'view';
450
+ if (currentType === seat_type) {
451
+ return toolResult(`Already on ${SEAT_LABELS[seat_type]} seat. No change needed.`);
452
+ }
453
+ const isUpgrade = (SEAT_HIERARCHY[seat_type] ?? 0) > (SEAT_HIERARCHY[currentType] ?? 0);
454
+ if (isUpgrade && !confirm) {
455
+ return toolError(`Upgrading from ${SEAT_LABELS[currentType]} to ${SEAT_LABELS[seat_type]} will increase billing. ` +
456
+ `Set confirm: true to authorize.`);
457
+ }
458
+ await internalClient(config).put(`/api/orgs/${orgId}/org_users`, {
459
+ org_user_ids: [String(member.id)],
460
+ paid_statuses: PAID_STATUSES[seat_type],
461
+ entry_point: 'members_tab',
462
+ seat_increase_authorized: 'true',
463
+ seat_swap_intended: 'false',
464
+ latest_ou_update: member.updated_at,
465
+ showing_billing_groups: 'true',
466
+ }, {
467
+ 'axios-retry': { retries: 0 },
468
+ });
469
+ return toolResult(JSON.stringify({
470
+ user: member.user?.handle || member.user?.email,
471
+ email: member.user?.email,
472
+ old_seat: SEAT_LABELS[currentType],
473
+ new_seat: SEAT_LABELS[seat_type],
474
+ }, null, 2));
475
+ }
476
+ catch (e) {
477
+ return toolError(`Failed to change seat: ${e.response?.status || e.message}`);
478
+ }
479
+ });
480
+ },
481
+ });
311
482
  //# sourceMappingURL=org.js.map
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "figmanage",
3
3
  "mcpName": "io.github.dannykeane/figmanage",
4
- "version": "0.1.2",
4
+ "version": "0.2.1",
5
5
  "description": "MCP server for managing your Figma workspace from the terminal.",
6
6
  "type": "module",
7
7
  "main": "dist/index.js",