Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 18 additions & 11 deletions docs/configuration.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Configuration Guide

The Figma MCP Write Server uses YAML configuration files for customizing server behavior, with support for command-line port overrides.
The Figma MCP Write Server uses YAML configuration files for customizing server behavior, with support for explicit command-line port overrides.

## Configuration File Location

Expand Down Expand Up @@ -124,21 +124,28 @@ The server supports these command-line arguments:
- **`--check-port <number>`** - Check if a port is available and show what's using it
- **`--help, -h`** - Show help message

### Port Management
### Port Requirements

The server includes automatic port management:
- Detects if the default port 8765 is in use
- Identifies and kills zombie processes when possible
- Falls back to alternative ports (8766, 8767, etc.) if needed
- Provides clear error messages for port conflicts
The plugin and MCP server must agree on one fixed WebSocket port.

- The default port is `8765`
- If that port is busy, the server now fails fast instead of silently moving to `8766`, `8767`, etc.
- This avoids a split-brain state where Claude talks to one server instance while the Figma plugin remains connected to another
- If you intentionally change the port, rebuild and re-import the plugin so its WebSocket target and allowed domains match the server

## Troubleshooting

### Port Conflicts
If port 8765 is in use:
1. Add `--port 9000` to the args array in your MCP configuration
2. Restart Claude Desktop
3. The server will use the specified port instead
1. Run `node dist/index.js --check-port 8765` to see which process is holding the port
2. Stop the conflicting process
3. Restart Claude Desktop so the MCP server can claim `8765`

If you intentionally want to use a non-default port:
1. Change the server port in your config or MCP args
2. Rebuild the plugin assets so the UI points at the same port
3. Update the plugin manifest allowed domains for that port
4. Re-import the plugin in Figma

### Font Database Issues
If font operations are slow:
Expand Down Expand Up @@ -199,4 +206,4 @@ fontDatabase:

- **Usage Examples**: See [examples.md](examples.md) for practical usage scenarios
- **Development**: See [development.md](development.md) for architecture and contribution guidelines
- **Advanced Topics**: See [architecture.md](architecture.md) for technical implementation details
- **Advanced Topics**: See [architecture.md](architecture.md) for technical implementation details
20 changes: 16 additions & 4 deletions figma-plugin/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,22 @@
"capabilities": [],
"enableProposedApi": true,
"enablePrivatePluginApi": true,
"permissions": ["currentuser", "activeusers"],
"editorType": ["figma"],
"permissions": [
"currentuser",
"activeusers"
],
"editorType": [
"figma",
"dev"
],
"networkAccess": {
"allowedDomains": ["*"],
"allowedDomains": [
"*"
],
"devAllowedDomains": [
"http://localhost:8765",
"ws://localhost:8765"
],
"reasoning": "This plugin needs to connect to the local MCP server via WebSocket for write operations."
}
}
}
3 changes: 2 additions & 1 deletion figma-plugin/src/operations/manage-nodes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -826,6 +826,7 @@ async function updateFrame(params: any): Promise<OperationResult> {

// Apply frame-specific properties
await applyFrameProperties(node as FrameNode, params, i);
await applyCommonNodeProperties(node, params, i);

results.push(formatNodeResponse(node));
} catch (error) {
Expand Down Expand Up @@ -1076,4 +1077,4 @@ function handleNodePositioning(
moveNodeToPosition(node, finalX, finalY);

return { warning, positionReason };
}
}
18 changes: 13 additions & 5 deletions figma-plugin/ui.template.html
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,11 @@
let debugMode = false;
let startTime = Date.now();
let uptimeTimer = null;
const connectionTargets = [
"ws://localhost:{{PORT}}",
"ws://127.0.0.1:{{PORT}}"
];
let currentTargetIndex = 0;

// Statistics
let stats = {
Expand Down Expand Up @@ -334,15 +339,17 @@

connectionAttempts++;
// Silent connection attempt
updateConnectionStatus(false, "Connecting...", true);
const target = connectionTargets[currentTargetIndex] || connectionTargets[0];
updateConnectionStatus(false, `Connecting to ${target.replace("ws://", "")}...`, true);

try {
ws = new WebSocket("ws://localhost:{{PORT}}");
ws = new WebSocket(target);

ws.onopen = () => {
// Silent connection success
updateConnectionStatus(true, "Connected to localhost:{{PORT}}");
updateConnectionStatus(true, `Connected to ${target.replace("ws://", "")}`);
connectionAttempts = 0;
currentTargetIndex = 0;
clearReconnectTimer();
ws.send(JSON.stringify({ type: "PLUGIN_HELLO", version: "{{VERSION}}" }));

Expand Down Expand Up @@ -381,7 +388,7 @@
ws.onerror = (error) => {
console.error("WebSocket error:", error);
log("WebSocket connection failed");
updateConnectionStatus(false, "Connection error");
updateConnectionStatus(false, `Connection error on ${target.replace("ws://", "")}`);
};
} catch (error) {
log(`Failed to connect: ${error.message}`);
Expand Down Expand Up @@ -409,6 +416,7 @@

reconnectTimer = setTimeout(() => {
log(`Reconnecting in ${delay}ms`);
currentTargetIndex = (currentTargetIndex + 1) % connectionTargets.length;
reconnectTimer = null;
connect();
}, delay);
Expand Down Expand Up @@ -557,4 +565,4 @@
setTimeout(connect, 1000);
</script>
</body>
</html>
</html>
12 changes: 6 additions & 6 deletions src/handlers/nodes-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export class NodeHandler implements ToolHandler {
return [
{
name: 'figma_nodes',
description: 'Create, get, update, delete, and duplicate geometric shape nodes. Returns YAML with node properties. Supports specialized operations for each node type: rectangles, ellipses, frames, sections, slices, stars, and polygons.',
description: 'Create, get, update, delete, and duplicate geometric shape nodes. Returns YAML with node properties. Supports specialized operations for each node type: rectangles, ellipses, frames, sections, slices, stars, and polygons. For gradient/image paints use figma_fills or figma_strokes; for shadows and blurs use figma_effects.',
inputSchema: {
type: 'object',
properties: {
Expand Down Expand Up @@ -73,14 +73,14 @@ export class NodeHandler implements ToolHandler {
{ type: 'string', pattern: '^#[0-9A-Fa-f]{6}$' },
{ type: 'array', items: { type: 'string', pattern: '^#[0-9A-Fa-f]{6}$' } }
],
description: 'Fill color(s) in hex format - single string or array for bulk operations'
description: 'Solid fill color(s) in hex format - single string or array for bulk operations. For gradients or images, use figma_fills.'
},
strokeColor: {
oneOf: [
{ type: 'string', pattern: '^#[0-9A-Fa-f]{6}$' },
{ type: 'array', items: { type: 'string', pattern: '^#[0-9A-Fa-f]{6}$' } }
],
description: 'Stroke color(s) in hex format - single string or array for bulk operations'
description: 'Solid stroke color(s) in hex format - single string or array for bulk operations. For gradients or images, use figma_strokes.'
},
fillOpacity: {
oneOf: [
Expand Down Expand Up @@ -314,7 +314,7 @@ export class NodeHandler implements ToolHandler {
'{"operation": "create_star", "name": "My Star", "pointCount": 6, "innerRadius": 0.4, "fillColor": "#FF0000", "strokeColor": "#0000FF", "strokeWeight": 3}',
'{"operation": "create_polygon", "name": "Octagon", "pointCount": 8, "fillColor": "#00FF00"}',
'{"operation": "update_rectangle", "nodeId": "123:456", "cornerRadius": 15, "topLeftRadius": 25}',
'{"operation": "update_frame", "nodeId": "123:789", "clipsContent": true, "cornerRadius": 8}',
'{"operation": "update_frame", "nodeId": "123:789", "clipsContent": true, "cornerRadius": 8, "strokeColor": "#222222", "strokeWeight": 2, "strokeAlign": "INSIDE"}',
'{"operation": "update_star", "nodeId": "123:999", "pointCount": 8, "innerRadius": 0.6}',
'{"operation": "update_section", "nodeId": "123:111", "devStatus": "IN_DEVELOPMENT"}'
]
Expand Down Expand Up @@ -398,7 +398,7 @@ export class NodeHandler implements ToolHandler {
// Specialized update operations
update_rectangle: ['nodeId', 'cornerRadius', 'topLeftRadius', 'topRightRadius', 'bottomLeftRadius', 'bottomRightRadius', 'cornerSmoothing'],
update_ellipse: ['nodeId'],
update_frame: ['nodeId', 'clipsContent', 'cornerRadius', 'topLeftRadius', 'topRightRadius', 'bottomLeftRadius', 'bottomRightRadius', 'cornerSmoothing'],
update_frame: ['nodeId', 'rotation', 'visible', 'locked', 'opacity', 'blendMode', 'fillColor', 'fillOpacity', 'strokeColor', 'strokeOpacity', 'strokeWeight', 'strokeAlign', 'clipsContent', 'cornerRadius', 'topLeftRadius', 'topRightRadius', 'bottomLeftRadius', 'bottomRightRadius', 'cornerSmoothing'],
update_section: ['nodeId', 'sectionContentsHidden', 'devStatus'],
update_slice: ['nodeId'],
update_star: ['nodeId', 'pointCount', 'innerRadius'],
Expand All @@ -409,4 +409,4 @@ export class NodeHandler implements ToolHandler {
return this.unifiedHandler.handle(args, config);
}

}
}
12 changes: 6 additions & 6 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ async function checkPortStatus(port: number): Promise<void> {
logger.log(` PID ${pid}: (process info unavailable)`);
}
}
logger.log(`💡 To kill these processes: kill -9 ${pids.join(' ')}`);
logger.log(`💡 Stop these processes before restarting the MCP server, or keep the port fixed and rebuild the plugin if you intentionally changed it.`);
}
} catch (error) {
logger.log(' (Unable to identify processes using this port)');
Expand Down Expand Up @@ -87,7 +87,7 @@ Options:

Description:
MCP server with built-in WebSocket server for Figma plugin communication.
Includes automatic port management with zombie process detection and cleanup.
Uses a fixed WebSocket port so the server stays aligned with the Figma plugin.

Architecture:
Claude Desktop ↔ MCP Server (WebSocket Server) ↔ Figma MCP Write Bridge Plugin (WebSocket Client)
Expand All @@ -99,10 +99,10 @@ Setup:
4. Use MCP tools from Claude Desktop

Port Management:
- Automatically detects if port 8765 is in use
- Identifies and kills zombie processes when possible
- Falls back to alternative ports (8766, 8767, etc.) if needed
- Provides clear error messages for port conflicts
- Uses port 8765 by default
- Fails fast if the configured port is already in use
- Use --check-port to inspect conflicts before restarting
- If you intentionally change ports, rebuild and re-import the plugin to match

Available MCP Tools:
25 comprehensive tools for Figma automation including:
Expand Down
39 changes: 20 additions & 19 deletions src/resources/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Resource, ResourceContents } from '@modelcontextprotocol/sdk/types';
import { readdir, readFile, stat, access } from 'fs/promises';
import { join, dirname, resolve } from 'path';
import { fileURLToPath } from 'url';
import { logger } from '../utils/logger.js';

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
Expand All @@ -16,31 +17,31 @@ export class ResourceRegistry {
constructor(sendToPluginFn: (request: any) => Promise<any>) {
// Use resolve to ensure absolute path and handle Windows paths correctly
this.resourcesPath = resolve(join(__dirname, '..', '..', 'mcp-resources'));
console.log(`[ResourceRegistry] Initialized with path: ${this.resourcesPath}`);
console.log(`[ResourceRegistry] __dirname: ${__dirname}`);
console.log(`[ResourceRegistry] Platform: ${process.platform}`);
console.log(`[ResourceRegistry] import.meta.url: ${import.meta.url}`);
logger.debug(`[ResourceRegistry] Initialized with path: ${this.resourcesPath}`);
logger.debug(`[ResourceRegistry] __dirname: ${__dirname}`);
logger.debug(`[ResourceRegistry] Platform: ${process.platform}`);
logger.debug(`[ResourceRegistry] import.meta.url: ${import.meta.url}`);
}

/**
* Get all available resources
*/
async getResources(): Promise<Resource[]> {
try {
console.log(`[ResourceRegistry] Scanning directory: ${this.resourcesPath}`);
logger.debug(`[ResourceRegistry] Scanning directory: ${this.resourcesPath}`);

// Check if directory exists and is accessible
try {
await access(this.resourcesPath);
console.log(`[ResourceRegistry] Directory is accessible`);
logger.debug(`[ResourceRegistry] Directory is accessible`);
} catch (accessError) {
console.error(`[ResourceRegistry] Directory not accessible:`, accessError);
logger.error(`[ResourceRegistry] Directory not accessible:`, accessError);
return [];
}

const resources: Resource[] = [];
const items = await readdir(this.resourcesPath);
console.log(`[ResourceRegistry] Found ${items.length} items:`, items);
logger.debug(`[ResourceRegistry] Found ${items.length} items:`, items);

for (const item of items) {
if (item.endsWith('.md')) {
Expand All @@ -52,15 +53,15 @@ export class ResourceRegistry {
mimeType: 'text/markdown',
description: `${title || item.replace('.md', '')} - Figma MCP usage guide`
};
console.log(`[ResourceRegistry] Adding resource:`, resource);
logger.debug(`[ResourceRegistry] Adding resource:`, resource);
resources.push(resource);
}
}

console.log(`[ResourceRegistry] Returning ${resources.length} resources`);
logger.debug(`[ResourceRegistry] Returning ${resources.length} resources`);
return resources;
} catch (error) {
console.error(`[ResourceRegistry] Error scanning resources:`, error);
logger.error(`[ResourceRegistry] Error scanning resources:`, error);
return [];
}
}
Expand All @@ -69,24 +70,24 @@ export class ResourceRegistry {
* Get content for a specific resource URI
*/
async getResourceContent(uri: string): Promise<ResourceContents> {
console.log(`[ResourceRegistry] Getting content for URI: ${uri}`);
logger.debug(`[ResourceRegistry] Getting content for URI: ${uri}`);

if (!uri.startsWith('figma://')) {
console.error(`[ResourceRegistry] Invalid URI scheme: ${uri}`);
logger.error(`[ResourceRegistry] Invalid URI scheme: ${uri}`);
throw new Error(`Resource not found: ${uri}`);
}

const filename = uri.replace('figma://', '');
const fullPath = resolve(join(this.resourcesPath, filename));
console.log(`[ResourceRegistry] Resolved file path: ${fullPath}`);
logger.debug(`[ResourceRegistry] Resolved file path: ${fullPath}`);

try {
// Check if file exists and is accessible
await access(fullPath);
console.log(`[ResourceRegistry] File is accessible: ${fullPath}`);
logger.debug(`[ResourceRegistry] File is accessible: ${fullPath}`);

const content = await readFile(fullPath, 'utf-8');
console.log(`[ResourceRegistry] Successfully read content (${content.length} chars)`);
logger.debug(`[ResourceRegistry] Successfully read content (${content.length} chars)`);

const result = {
uri: uri,
Expand All @@ -98,10 +99,10 @@ export class ResourceRegistry {
}
]
};
console.log(`[ResourceRegistry] Returning resource content for: ${uri}`);
logger.debug(`[ResourceRegistry] Returning resource content for: ${uri}`);
return result;
} catch (error) {
console.error(`[ResourceRegistry] Error reading file ${fullPath}:`, error);
logger.error(`[ResourceRegistry] Error reading file ${fullPath}:`, error);
throw new Error(`Resource not found: ${uri}`);
}
}
Expand Down Expand Up @@ -138,4 +139,4 @@ export class ResourceRegistry {
return null;
}
}
}
}
Loading