Technical reference for contributors.
- MCP Server (
src/): Handles tool registration and Claude communication - WebSocket Bridge (
src/services/websocket.ts): Connects to Figma plugin - Handlers (
src/handlers/): Tool implementations organized by domain - Font Database (
src/services/font-database.ts): SQLite-based font search
- Main Thread (
figma-plugin/src/): Figma API access, no browser APIs - UI Thread (
figma-plugin/ui/): Browser APIs, no Figma access - Operations (
figma-plugin/src/operations/): Auto-discovered handlers
Claude → MCP Server → WebSocket → UI Thread → Main Plugin Thread → Figma API
- Create handler in
figma-plugin/src/operations/my-operation.ts:
export async function MY_OPERATION(payload: {param: string}): Promise<any> {
// Access Figma API here
const node = figma.createRectangle();
node.name = payload.param;
return { id: node.id };
}- Add MCP method in appropriate handler:
async myNewTool(args: {param: string}): Promise<string> {
const result = await this.executeOperation('MY_OPERATION', args);
return formatYamlResponse(result);
}- Register in
getTools():
{
name: 'my_new_tool',
description: 'Does something new',
inputSchema: {
type: 'object',
properties: {
param: { type: 'string', description: 'Parameter description' }
},
required: ['param']
}
}geometry-handler.ts: Shapes, fills, strokes, effectstext-handler.ts: Text creation and stylinglayout-handler.ts: Auto layout, constraints, alignmentdesign-system-handler.ts: Styles, components, variablesadvanced-handler.ts: Boolean ops, vectors, exportssystem-handler.ts: Plugin status, diagnostics
// WRONG - Direct mutation doesn't trigger updates
node.fills.push(newFill);
// CORRECT - Clone, modify, reassign
import { modifyFills } from '../utils/figma-property-utils.js';
modifyFills(node, manager => {
manager.push(newFill);
});// Main thread - no atob/btoa available
figma.ui.postMessage({
type: 'DECODE_BASE64',
data: base64String
});
// UI thread - has browser APIs
if (msg.type === 'DECODE_BASE64') {
const bytes = atob(msg.data);
// Process bytes...
}// In operations
throw new Error('Specific error message');
// In handlers
try {
const result = await this.executeOperation(...);
return formatYamlResponse(result);
} catch (error) {
throw error; // Let MCP handle it
}// Pattern: Single MCP tool, multiple internal operations
case 'create': {
// Step 1: Delegate to existing operation
const { MANAGE_NODES } = await import('../generated-operations.js');
const nodeResult = await MANAGE_NODES({
operation: 'create_rectangle',
width: 10, height: 10, x, y
});
// Step 2: Delegate to another operation
const { MANAGE_FILLS } = await import('../generated-operations.js');
const fillResult = await MANAGE_FILLS({
operation: 'add_image',
nodeId: nodeResult.nodeId,
imageUrl: url
});
// Step 3: Update with final result
await MANAGE_NODES({
operation: 'update',
nodeId: nodeResult.nodeId,
width: fillResult.dimensions.width,
height: fillResult.dimensions.height
});
// Return combined result
return {
success: true,
nodeId: nodeResult.nodeId,
imageHash: fillResult.imageHash,
// ... combined metadata
};
}Benefits:
- Agent Simplicity: Single tool call instead of orchestrating multiple operations
- Code Reuse: Leverages existing operation logic (DRY principle)
- Atomic Behavior: Either complete success or rollback
- Performance: Single WebSocket roundtrip instead of multiple
Example tools: figma_images (create operation)
npm run test:unit- Test individual functions
- Mock Figma API
- Located in
tests/unit/
npm run test:integration- Test full tool flow
- Mock WebSocket connection
- Located in
tests/integration/
npm run test:manualProvides step-by-step testing guide.
npm run dev # Watch mode
npm run dev:plugin # Plugin only
npm run dev:server # Server onlynpm run build # Build all
npm run build:plugin
npm run build:server- TypeScript compilation
- Inline CSS into HTML
- Bundle with esbuild
- Copy manifest.json
- Ensure plugin is running in Figma
- Check port isn't blocked (default: 8765)
- Verify no other instances running
- Operation name must match export name
- File must be in
operations/directory - Check for typos in UPPER_SNAKE_CASE
- Some properties are read-only
- Parent constraints affect operations
- Check node type compatibility
- Use
figma.d.tsfor API types - Import from
@figma/plugin-typings - Check for null/undefined nodes
- Combine multiple updates in single operation
- Use
figma.commitUndo()for atomic changes - Minimize plugin<->UI communication
- Process in chunks for 100+ nodes
- Show progress for long operations
- Use
figma.currentPage.selectionefficiently
- SQLite indexes on family, style, weight
- Cached for 24 hours by default
- Background sync to avoid blocking
- Update version in
package.json - Run tests:
npm test - Build:
npm run build - Update CHANGELOG.md
- Commit with format:
type: description (vX.X.X)
- Change 1
- Change 2
Designed with ❤️ by oO. Coded with ✨ by Claude Sonnet 4
Co-authored-by: Claude.AI <noreply@anthropic.com>