conductor-figma 0.1.0 → 0.2.0

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/bin/conductor.js CHANGED
@@ -3,21 +3,27 @@
3
3
  import { startServer } from '../src/server.js';
4
4
  import { TOOLS, CATEGORIES } from '../src/tools/registry.js';
5
5
 
6
- const args = process.argv.slice(2);
6
+ var args = process.argv.slice(2);
7
7
 
8
8
  if (args.includes('--help') || args.includes('-h')) {
9
9
  console.log(`
10
10
  ⊞ CONDUCTOR — Design-intelligent MCP server for Figma
11
11
 
12
12
  Usage:
13
- conductor-figma Start MCP server (stdio)
14
- conductor-figma --list List all ${TOOLS.length} tools
15
- conductor-figma --categories Show tool categories
16
- conductor-figma --help Show this help
13
+ conductor-figma Start MCP server + Figma relay
14
+ conductor-figma --port 9800 Set WebSocket port (default: 9800)
15
+ conductor-figma --list List all ${TOOLS.length} tools
16
+ conductor-figma --categories Show tool categories
17
+ conductor-figma --help Show this help
17
18
 
18
- MCP Setup (Cursor):
19
- Add to ~/.cursor/mcp.json:
19
+ How it works:
20
+ 1. Cursor sends tool calls via MCP (stdio)
21
+ 2. Design tools (color, type, spacing) resolve locally
22
+ 3. Figma tools forward through WebSocket to the plugin
23
+ 4. Plugin executes on canvas, results flow back
20
24
 
25
+ Setup:
26
+ ~/.cursor/mcp.json:
21
27
  {
22
28
  "mcpServers": {
23
29
  "conductor": {
@@ -27,32 +33,43 @@ if (args.includes('--help') || args.includes('-h')) {
27
33
  }
28
34
  }
29
35
 
30
- ${TOOLS.length} tools · ${Object.keys(CATEGORIES).length} categories · Zero dependencies
36
+ Then open the CONDUCTOR plugin in Figma and click Connect.
37
+
38
+ ${TOOLS.length} tools · ${Object.keys(CATEGORIES).length} categories
31
39
  Built by 0xDragoon · MIT License
32
40
  `);
33
41
  process.exit(0);
34
42
  }
35
43
 
36
44
  if (args.includes('--list')) {
37
- for (const [catKey, cat] of Object.entries(CATEGORIES)) {
38
- const tools = TOOLS.filter(t => t.category === catKey);
39
- console.log(`\n ${cat.icon} ${cat.label} (${tools.length})`);
40
- for (const t of tools) {
41
- console.log(` ${t.name.padEnd(28)} ${t.description.slice(0, 70)}`);
45
+ for (var entries = Object.entries(CATEGORIES), i = 0; i < entries.length; i++) {
46
+ var catKey = entries[i][0], cat = entries[i][1];
47
+ var tools = TOOLS.filter(function(t) { return t.category === catKey; });
48
+ console.log('\n ' + cat.icon + ' ' + cat.label + ' (' + tools.length + ')');
49
+ for (var j = 0; j < tools.length; j++) {
50
+ console.log(' ' + tools[j].name.padEnd(28) + ' ' + tools[j].description.slice(0, 70));
42
51
  }
43
52
  }
44
- console.log(`\n ${TOOLS.length} tools total\n`);
53
+ console.log('\n ' + TOOLS.length + ' tools total\n');
45
54
  process.exit(0);
46
55
  }
47
56
 
48
57
  if (args.includes('--categories')) {
49
- for (const [key, cat] of Object.entries(CATEGORIES)) {
50
- const tools = TOOLS.filter(t => t.category === key);
51
- console.log(` ${cat.icon} ${cat.label.padEnd(18)} ${tools.length} tools`);
58
+ for (var entries2 = Object.entries(CATEGORIES), k = 0; k < entries2.length; k++) {
59
+ var key = entries2[k][0], cat2 = entries2[k][1];
60
+ var count = TOOLS.filter(function(t) { return t.category === key; }).length;
61
+ console.log(' ' + cat2.icon + ' ' + cat2.label.padEnd(18) + ' ' + count + ' tools');
52
62
  }
53
- console.log(`\n ${TOOLS.length} tools total`);
63
+ console.log('\n ' + TOOLS.length + ' tools total');
54
64
  process.exit(0);
55
65
  }
56
66
 
57
- // Default: start MCP server
58
- startServer();
67
+ // Parse port
68
+ var port = 9800;
69
+ var portIdx = args.indexOf('--port');
70
+ if (portIdx !== -1 && args[portIdx + 1]) {
71
+ port = parseInt(args[portIdx + 1]) || 9800;
72
+ }
73
+
74
+ // Start MCP server with relay
75
+ startServer({ port: port });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "conductor-figma",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Design-intelligent MCP server for Figma. 61 tools across 10 categories. 8px grid, type scale ratios, auto-layout, component reuse, accessibility — real design intelligence, not shape proxying.",
5
5
  "author": "0xDragoon",
6
6
  "license": "MIT",
@@ -33,5 +33,8 @@
33
33
  "engines": {
34
34
  "node": ">=18.0.0"
35
35
  },
36
- "dependencies": {}
36
+ "dependencies": {},
37
+ "optionalDependencies": {
38
+ "ws": "^8.0.0"
39
+ }
37
40
  }
package/src/index.js CHANGED
@@ -3,6 +3,7 @@
3
3
  // ═══════════════════════════════════════════
4
4
 
5
5
  export { startServer } from './server.js';
6
+ export { Relay } from './relay.js';
6
7
  export { TOOLS, CATEGORIES, getToolByName, getToolsByCategory, getAllToolNames } from './tools/registry.js';
7
8
  export { handleTool } from './tools/handlers.js';
8
9
 
package/src/relay.js ADDED
@@ -0,0 +1,174 @@
1
+ // ═══════════════════════════════════════════
2
+ // CONDUCTOR — WebSocket Relay
3
+ // ═══════════════════════════════════════════
4
+ // Bridges MCP stdio (from Cursor) to WebSocket (to Figma plugin).
5
+ //
6
+ // Flow:
7
+ // Cursor → MCP stdio → CONDUCTOR → design logic (local)
8
+ // → figma commands (WebSocket) → Plugin → Figma API
9
+ // ← results ← WebSocket ←
10
+ // ← MCP stdio ← CONDUCTOR ←
11
+
12
+ import { createServer } from 'node:http';
13
+
14
+ // Tools that need Figma (sent over WebSocket to plugin)
15
+ const FIGMA_COMMANDS = new Set([
16
+ // Create
17
+ 'create_frame', 'create_text', 'create_rect', 'create_section', 'create_component',
18
+ // Layout
19
+ 'set_auto_layout', 'set_constraints', 'apply_grid', 'align_nodes',
20
+ // Style
21
+ 'set_fills', 'set_strokes', 'set_effects', 'set_corner_radius', 'set_opacity',
22
+ // Typography
23
+ 'set_text_props', 'load_font',
24
+ // Read
25
+ 'get_selection', 'get_page_info', 'get_styles', 'get_components',
26
+ 'read_node', 'read_tree', 'read_spacing', 'read_colors', 'read_typography',
27
+ // Edit
28
+ 'rename_node', 'move_node', 'resize_node', 'delete_node',
29
+ 'clone_node', 'group_nodes', 'ungroup_node', 'reorder_node',
30
+ // Export
31
+ 'export_png', 'export_svg',
32
+ // Viewport
33
+ 'zoom_to', 'scroll_to',
34
+ // Meta
35
+ 'ping',
36
+ ]);
37
+
38
+ export class Relay {
39
+ constructor(port) {
40
+ this.port = port || 9800;
41
+ this.pluginSocket = null;
42
+ this.pendingCallbacks = new Map();
43
+ this.cmdId = 0;
44
+ this.server = null;
45
+ this.wss = null;
46
+ }
47
+
48
+ async start() {
49
+ // Dynamic import ws (may not be installed — we bundle our own minimal WS)
50
+ let WebSocketServer;
51
+ try {
52
+ const ws = await import('ws');
53
+ WebSocketServer = ws.WebSocketServer || ws.default.WebSocketServer;
54
+ } catch (e) {
55
+ process.stderr.write('CONDUCTOR relay: "ws" package not found. Install with: npm install ws\n');
56
+ process.stderr.write('Falling back to MCP-only mode (no Figma bridge).\n');
57
+ return false;
58
+ }
59
+
60
+ this.server = createServer();
61
+ this.wss = new WebSocketServer({ server: this.server });
62
+
63
+ this.wss.on('connection', (socket) => {
64
+ this.pluginSocket = socket;
65
+ process.stderr.write('CONDUCTOR relay: Figma plugin connected\n');
66
+
67
+ socket.on('message', (data) => {
68
+ try {
69
+ const msg = JSON.parse(data.toString());
70
+ this.handlePluginMessage(msg);
71
+ } catch (e) {
72
+ // ignore
73
+ }
74
+ });
75
+
76
+ socket.on('close', () => {
77
+ this.pluginSocket = null;
78
+ process.stderr.write('CONDUCTOR relay: Figma plugin disconnected\n');
79
+ });
80
+
81
+ socket.on('error', () => {
82
+ this.pluginSocket = null;
83
+ });
84
+ });
85
+
86
+ return new Promise((resolve) => {
87
+ this.server.listen(this.port, () => {
88
+ process.stderr.write(`CONDUCTOR relay: WebSocket listening on ws://localhost:${this.port}\n`);
89
+ resolve(true);
90
+ });
91
+ });
92
+ }
93
+
94
+ handlePluginMessage(msg) {
95
+ if (msg.type === 'plugin_ready') {
96
+ process.stderr.write(`CONDUCTOR relay: Plugin ready (v${msg.version || '?'})\n`);
97
+ return;
98
+ }
99
+
100
+ if (msg.type === 'result' && msg.id !== undefined) {
101
+ const callback = this.pendingCallbacks.get(msg.id);
102
+ if (callback) {
103
+ this.pendingCallbacks.delete(msg.id);
104
+ callback(msg.data || {});
105
+ }
106
+ return;
107
+ }
108
+ }
109
+
110
+ isConnected() {
111
+ return this.pluginSocket !== null && this.pluginSocket.readyState === 1; // WebSocket.OPEN
112
+ }
113
+
114
+ /**
115
+ * Send a command to the Figma plugin and wait for result.
116
+ * Returns a Promise that resolves with the plugin's response.
117
+ */
118
+ sendToPlugin(commandType, commandData, timeout) {
119
+ timeout = timeout || 15000;
120
+
121
+ return new Promise((resolve, reject) => {
122
+ if (!this.isConnected()) {
123
+ resolve({ error: 'Figma plugin not connected. Open the CONDUCTOR plugin in Figma and click Connect.' });
124
+ return;
125
+ }
126
+
127
+ var id = ++this.cmdId;
128
+ var timer = setTimeout(function() {
129
+ this.pendingCallbacks.delete(id);
130
+ resolve({ error: 'Timeout waiting for Figma plugin response' });
131
+ }.bind(this), timeout);
132
+
133
+ this.pendingCallbacks.set(id, function(result) {
134
+ clearTimeout(timer);
135
+ resolve(result);
136
+ });
137
+
138
+ this.pluginSocket.send(JSON.stringify({
139
+ type: 'command',
140
+ id: id,
141
+ command: { type: commandType, data: commandData || {} },
142
+ }));
143
+ });
144
+ }
145
+
146
+ /**
147
+ * Check if a tool name maps to a Figma command.
148
+ */
149
+ isFigmaCommand(toolName) {
150
+ return FIGMA_COMMANDS.has(toolName);
151
+ }
152
+
153
+ /**
154
+ * Map a high-level tool call to one or more Figma commands.
155
+ * Some tools (like create_page) produce multiple Figma commands.
156
+ * Some tools (like color_palette) are pure design logic — no Figma needed.
157
+ */
158
+ getFigmaCommand(toolName, toolArgs) {
159
+ // Direct mappings — tool name IS the Figma command
160
+ if (FIGMA_COMMANDS.has(toolName)) {
161
+ return { command: toolName, data: toolArgs };
162
+ }
163
+
164
+ // Tools that generate Figma commands from design logic output
165
+ // The handler produces a JSON response with an "action" field
166
+ // that maps to a Figma command
167
+ return null;
168
+ }
169
+
170
+ stop() {
171
+ if (this.wss) this.wss.close();
172
+ if (this.server) this.server.close();
173
+ }
174
+ }
package/src/server.js CHANGED
@@ -1,115 +1,141 @@
1
1
  // ═══════════════════════════════════════════
2
- // CONDUCTOR — MCP Server
2
+ // CONDUCTOR — MCP Server + Relay Bridge
3
3
  // ═══════════════════════════════════════════
4
- // Model Context Protocol server over stdio.
5
- // Registers 61 design-intelligent tools for AI editors.
4
+ // MCP protocol over stdio (for Cursor/Claude Code).
5
+ // WebSocket relay to Figma plugin (for canvas operations).
6
+ //
7
+ // Pure design tools (color_palette, type_scale, etc.) resolve locally.
8
+ // Figma tools (create_frame, read_node, etc.) forward through WebSocket.
6
9
 
7
10
  import { TOOLS } from './tools/registry.js';
8
11
  import { handleTool } from './tools/handlers.js';
12
+ import { Relay } from './relay.js';
9
13
 
10
- const SERVER_INFO = {
11
- name: 'conductor-figma',
12
- version: '0.1.0',
13
- };
14
+ var SERVER_INFO = { name: 'conductor-figma', version: '0.2.0' };
15
+ var CAPABILITIES = { tools: {} };
16
+ var relay = null;
14
17
 
15
- const CAPABILITIES = {
16
- tools: {},
17
- };
18
+ export async function startServer(options) {
19
+ options = options || {};
20
+ var port = options.port || 9800;
18
21
 
19
- /**
20
- * Start the MCP server on stdio.
21
- * Reads JSON-RPC messages from stdin, writes responses to stdout.
22
- */
23
- export function startServer() {
24
- let buffer = '';
22
+ relay = new Relay(port);
23
+ var relayStarted = await relay.start();
24
+
25
+ if (relayStarted) {
26
+ process.stderr.write('CONDUCTOR: MCP + relay started (' + TOOLS.length + ' tools, ws://localhost:' + port + ')\n');
27
+ } else {
28
+ process.stderr.write('CONDUCTOR: MCP started (' + TOOLS.length + ' tools, no relay — install ws for Figma bridge)\n');
29
+ }
30
+
31
+ var buffer = '';
25
32
 
26
33
  process.stdin.setEncoding('utf-8');
27
- process.stdin.on('data', (chunk) => {
34
+ process.stdin.on('data', function(chunk) {
28
35
  buffer += chunk;
29
-
30
- // Process complete lines
31
- const lines = buffer.split('\n');
36
+ var lines = buffer.split('\n');
32
37
  buffer = lines.pop() || '';
33
38
 
34
- for (const line of lines) {
35
- const trimmed = line.trim();
39
+ for (var i = 0; i < lines.length; i++) {
40
+ var trimmed = lines[i].trim();
36
41
  if (!trimmed) continue;
37
-
38
42
  try {
39
- const message = JSON.parse(trimmed);
40
- handleMessage(message);
43
+ handleMessage(JSON.parse(trimmed));
41
44
  } catch (err) {
42
45
  sendError(null, -32700, 'Parse error');
43
46
  }
44
47
  }
45
48
  });
46
49
 
47
- process.stdin.on('end', () => {
50
+ process.stdin.on('end', function() {
51
+ if (relay) relay.stop();
48
52
  process.exit(0);
49
53
  });
50
-
51
- // Log startup to stderr (not stdout — that's for MCP protocol)
52
- process.stderr.write(`CONDUCTOR MCP server started (${TOOLS.length} tools)\n`);
53
54
  }
54
55
 
55
- function handleMessage(msg) {
56
- // JSON-RPC 2.0
57
- const { id, method, params } = msg;
56
+ async function handleMessage(msg) {
57
+ var id = msg.id;
58
+ var method = msg.method;
59
+ var params = msg.params || {};
58
60
 
59
61
  switch (method) {
60
62
  case 'initialize':
61
- sendResult(id, {
62
- protocolVersion: '2024-11-05',
63
- serverInfo: SERVER_INFO,
64
- capabilities: CAPABILITIES,
65
- });
63
+ sendResult(id, { protocolVersion: '2024-11-05', serverInfo: SERVER_INFO, capabilities: CAPABILITIES });
66
64
  break;
67
65
 
68
66
  case 'initialized':
69
- // Notification, no response needed
70
67
  break;
71
68
 
72
69
  case 'tools/list':
73
70
  sendResult(id, {
74
- tools: TOOLS.map(t => ({
75
- name: t.name,
76
- description: t.description,
77
- inputSchema: t.inputSchema,
78
- })),
71
+ tools: TOOLS.map(function(t) { return { name: t.name, description: t.description, inputSchema: t.inputSchema }; }),
79
72
  });
80
73
  break;
81
74
 
82
- case 'tools/call': {
83
- const toolName = params?.name;
84
- const toolArgs = params?.arguments || {};
85
-
86
- if (!toolName) {
87
- sendError(id, -32602, 'Missing tool name');
88
- return;
89
- }
90
-
91
- const result = handleTool(toolName, toolArgs, null);
92
- sendResult(id, result);
75
+ case 'tools/call':
76
+ var toolName = params.name;
77
+ var toolArgs = params.arguments || {};
78
+ if (!toolName) { sendError(id, -32602, 'Missing tool name'); return; }
79
+ await handleToolCall(id, toolName, toolArgs);
93
80
  break;
94
- }
95
81
 
96
82
  case 'ping':
97
83
  sendResult(id, {});
98
84
  break;
99
85
 
100
86
  default:
101
- if (id !== undefined) {
102
- sendError(id, -32601, `Method not found: ${method}`);
87
+ if (id !== undefined) sendError(id, -32601, 'Method not found: ' + method);
88
+ }
89
+ }
90
+
91
+ async function handleToolCall(id, toolName, toolArgs) {
92
+ // If tool is a direct Figma command and plugin is connected — forward it
93
+ if (relay && relay.isFigmaCommand(toolName) && relay.isConnected()) {
94
+ process.stderr.write('CONDUCTOR: -> Figma: ' + toolName + '\n');
95
+ try {
96
+ var figmaResult = await relay.sendToPlugin(toolName, toolArgs);
97
+ process.stderr.write('CONDUCTOR: <- Figma: ' + (figmaResult.name || figmaResult.id || 'ok') + '\n');
98
+ sendResult(id, { content: [{ type: 'text', text: JSON.stringify(figmaResult, null, 2) }] });
99
+ } catch (err) {
100
+ sendResult(id, { content: [{ type: 'text', text: JSON.stringify({ error: String(err) }) }] });
101
+ }
102
+ return;
103
+ }
104
+
105
+ // Run through design intelligence handler
106
+ var result = handleTool(toolName, toolArgs, null);
107
+
108
+ // Check if handler produced a Figma action we should forward
109
+ if (relay && relay.isConnected()) {
110
+ try {
111
+ var data = JSON.parse(result.content[0].text);
112
+ if (data.action && relay.isFigmaCommand(data.action)) {
113
+ process.stderr.write('CONDUCTOR: -> Figma (via ' + toolName + '): ' + data.action + '\n');
114
+ var fResult = await relay.sendToPlugin(data.action, data);
115
+ process.stderr.write('CONDUCTOR: <- Figma: ' + (fResult.name || fResult.id || 'ok') + '\n');
116
+ data._figmaResult = fResult;
117
+ sendResult(id, { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] });
118
+ return;
103
119
  }
120
+ } catch (e) { /* not JSON or no action */ }
104
121
  }
122
+
123
+ // If Figma command but plugin not connected — add note
124
+ if (relay && relay.isFigmaCommand(toolName) && !relay.isConnected()) {
125
+ try {
126
+ var parsed = JSON.parse(result.content[0].text);
127
+ parsed._note = 'Figma plugin not connected. Connect the CONDUCTOR plugin in Figma to execute on canvas.';
128
+ result = { content: [{ type: 'text', text: JSON.stringify(parsed, null, 2) }] };
129
+ } catch (e) { /* ignore */ }
130
+ }
131
+
132
+ sendResult(id, result);
105
133
  }
106
134
 
107
135
  function sendResult(id, result) {
108
- const response = { jsonrpc: '2.0', id, result };
109
- process.stdout.write(JSON.stringify(response) + '\n');
136
+ process.stdout.write(JSON.stringify({ jsonrpc: '2.0', id: id, result: result }) + '\n');
110
137
  }
111
138
 
112
139
  function sendError(id, code, message) {
113
- const response = { jsonrpc: '2.0', id, error: { code, message } };
114
- process.stdout.write(JSON.stringify(response) + '\n');
140
+ process.stdout.write(JSON.stringify({ jsonrpc: '2.0', id: id, error: { code: code, message: message } }) + '\n');
115
141
  }