conductor-figma 1.0.2 → 3.0.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/src/server.js CHANGED
@@ -1,217 +1,103 @@
1
1
  // ═══════════════════════════════════════════
2
- // CONDUCTOR — MCP Server + Orchestrator
2
+ // CONDUCTOR v3 — MCP Server (stdio)
3
3
  // ═══════════════════════════════════════════
4
- // When create_page or create_section is called:
5
- // 1. Generates a blueprint (30-50 sequential commands)
6
- // 2. Executes each one through the relay to the Figma plugin
7
- // 3. Each command references results from previous commands ($ref)
8
- // 4. Returns a summary of everything created
9
-
10
- import { TOOLS } from './tools/registry.js';
11
- import { handleTool } from './tools/handlers.js';
12
- import { Relay } from './relay.js';
13
- import { getBlueprint } from './blueprints.js';
14
- import { executeSequence } from './orchestrator.js';
15
-
16
- var SERVER_INFO = { name: 'conductor-figma', version: '0.3.0' };
17
- var CAPABILITIES = { tools: {} };
18
- var relay = null;
19
-
20
- // Tools that trigger blueprint orchestration (multi-command sequences)
21
- var BLUEPRINT_TOOLS = new Set(['create_page', 'create_section']);
22
-
23
- export async function startServer(options) {
24
- options = options || {};
25
- var port = options.port || 9800;
26
-
27
- relay = new Relay(port);
28
- var relayStarted = await relay.start();
29
-
30
- if (relayStarted) {
31
- process.stderr.write('CONDUCTOR v0.3.0: MCP + orchestrator ready (' + TOOLS.length + ' tools, ws://localhost:' + port + ')\n');
32
- } else {
33
- process.stderr.write('CONDUCTOR v0.3.0: MCP ready (' + TOOLS.length + ' tools, no relay — install ws)\n');
34
- }
35
-
36
- var buffer = '';
37
-
38
- process.stdin.setEncoding('utf-8');
39
- process.stdin.on('data', function(chunk) {
40
- buffer += chunk;
41
- var lines = buffer.split('\n');
42
- buffer = lines.pop() || '';
43
4
 
44
- for (var i = 0; i < lines.length; i++) {
45
- var trimmed = lines[i].trim();
46
- if (!trimmed) continue;
47
- try {
48
- handleMessage(JSON.parse(trimmed));
49
- } catch (err) {
50
- sendError(null, -32700, 'Parse error');
51
- }
52
- }
53
- });
5
+ import { TOOL_LIST, TOOL_COUNT, getTool, CATEGORIES } from './tools/registry.js'
6
+ import { handleTool } from './tools/handlers.js'
7
+ import { createBridge } from './bridge.js'
8
+
9
+ const VERSION = '3.0.0'
10
+ let bridge = null
11
+
12
+ function log(...args) { process.stderr.write('[conductor] ' + args.join(' ') + '\n') }
13
+
14
+ // ─── JSON-RPC over stdio ───
15
+ let buffer = ''
16
+
17
+ process.stdin.setEncoding('utf8')
18
+ process.stdin.on('data', chunk => {
19
+ buffer += chunk
20
+ while (true) {
21
+ const headerEnd = buffer.indexOf('\r\n\r\n')
22
+ if (headerEnd === -1) break
23
+ const header = buffer.slice(0, headerEnd)
24
+ const match = header.match(/Content-Length:\s*(\d+)/i)
25
+ if (!match) { buffer = buffer.slice(headerEnd + 4); continue }
26
+ const len = parseInt(match[1])
27
+ const bodyStart = headerEnd + 4
28
+ if (buffer.length < bodyStart + len) break
29
+ const body = buffer.slice(bodyStart, bodyStart + len)
30
+ buffer = buffer.slice(bodyStart + len)
31
+ try {
32
+ const msg = JSON.parse(body)
33
+ handleMessage(msg)
34
+ } catch (e) { log('Parse error:', e.message) }
35
+ }
36
+ })
54
37
 
55
- process.stdin.on('end', function() {
56
- if (relay) relay.stop();
57
- process.exit(0);
58
- });
38
+ function send(msg) {
39
+ const json = JSON.stringify(msg)
40
+ const out = `Content-Length: ${Buffer.byteLength(json)}\r\n\r\n${json}`
41
+ process.stdout.write(out)
59
42
  }
60
43
 
44
+ function respond(id, result) { send({ jsonrpc: '2.0', id, result }) }
45
+ function respondError(id, code, message) { send({ jsonrpc: '2.0', id, error: { code, message } }) }
46
+
61
47
  async function handleMessage(msg) {
62
- var id = msg.id;
63
- var method = msg.method;
64
- var params = msg.params || {};
48
+ const { id, method, params } = msg
65
49
 
66
50
  switch (method) {
67
51
  case 'initialize':
68
- sendResult(id, { protocolVersion: '2024-11-05', serverInfo: SERVER_INFO, capabilities: CAPABILITIES });
69
- break;
52
+ if (!bridge) {
53
+ bridge = createBridge()
54
+ await bridge.start()
55
+ }
56
+ return respond(id, {
57
+ protocolVersion: '2024-11-05',
58
+ capabilities: { tools: { listChanged: false } },
59
+ serverInfo: { name: 'conductor-figma', version: VERSION },
60
+ })
70
61
 
71
- case 'initialized':
72
- break;
62
+ case 'notifications/initialized':
63
+ log(`Conductor v${VERSION} ready — ${TOOL_COUNT} tools across ${Object.keys(CATEGORIES).length} categories`)
64
+ return
73
65
 
74
66
  case 'tools/list':
75
- // Only expose high-level tools to Cursor.
76
- // Low-level Figma commands (create_frame, create_text, etc.) are hidden
77
- // because the orchestrator uses them internally via blueprints.
78
- var HIDDEN_FROM_AI = new Set([
79
- 'create_frame', 'create_text', 'create_rect', 'create_ellipse', 'create_line',
80
- 'create_svg_node', 'create_card', 'create_form', 'create_table', 'create_modal', 'create_nav',
81
- 'layout_auto', 'layout_grid', 'layout_stack', 'layout_wrap', 'layout_constrain', 'layout_align', 'layout_nest',
82
- 'set_fills', 'set_strokes', 'set_effects', 'set_corner_radius', 'set_opacity',
83
- 'set_text_props', 'load_font', 'style_text_range',
84
- 'rename_node', 'move_node', 'resize_node', 'delete_node', 'clone_node', 'group_nodes', 'ungroup_node', 'reorder_node',
85
- 'find_nodes',
86
- ]);
87
- var visibleTools = TOOLS.filter(function(t) { return !HIDDEN_FROM_AI.has(t.name); });
88
- sendResult(id, {
89
- tools: visibleTools.map(function(t) { return { name: t.name, description: t.description, inputSchema: t.inputSchema }; }),
90
- });
91
- break;
92
-
93
- case 'tools/call':
94
- var toolName = params.name;
95
- var toolArgs = params.arguments || {};
96
- if (!toolName) { sendError(id, -32602, 'Missing tool name'); return; }
97
- await handleToolCall(id, toolName, toolArgs);
98
- break;
67
+ return respond(id, {
68
+ tools: TOOL_LIST.map(t => ({
69
+ name: t.name,
70
+ description: t.description,
71
+ inputSchema: t.inputSchema,
72
+ }))
73
+ })
74
+
75
+ case 'tools/call': {
76
+ const { name, arguments: args } = params
77
+ const tool = getTool(name)
78
+ if (!tool) return respondError(id, -32601, `Unknown tool: ${name}`)
99
79
 
100
- case 'ping':
101
- sendResult(id, {});
102
- break;
103
-
104
- default:
105
- if (id !== undefined) sendError(id, -32601, 'Method not found: ' + method);
106
- }
107
- }
108
-
109
- async function handleToolCall(id, toolName, toolArgs) {
110
-
111
- // ═══ ORCHESTRATED BLUEPRINTS ═══
112
- // create_page and create_section generate 20-50 commands and execute them all
113
- if (BLUEPRINT_TOOLS.has(toolName) && relay && relay.isConnected()) {
114
- var blueprint = getBlueprint(toolName, toolArgs);
115
-
116
- if (blueprint && blueprint.commands && blueprint.commands.length > 0) {
117
- process.stderr.write('CONDUCTOR orchestrator: ' + toolName + ' -> ' + blueprint.commands.length + ' commands\n');
118
- process.stderr.write('CONDUCTOR orchestrator: ' + blueprint.description + '\n');
119
-
120
- var outcome = await executeSequence(relay, blueprint.commands);
121
-
122
- var summary = {
123
- tool: toolName,
124
- description: blueprint.description,
125
- totalCommands: outcome.totalSteps,
126
- completed: outcome.completedSteps,
127
- errors: outcome.errors.length,
128
- success: outcome.success,
129
- createdNodes: [],
130
- };
131
-
132
- // Collect all created node IDs and names
133
- for (var r = 0; r < outcome.results.length; r++) {
134
- var res = outcome.results[r];
135
- if (res && res.id) {
136
- summary.createdNodes.push({ id: res.id, name: res.name || '', type: res.type || '' });
137
- }
138
- }
139
-
140
- if (outcome.errors.length > 0) {
141
- summary.errorDetails = outcome.errors;
80
+ try {
81
+ const result = await handleTool(name, args || {}, bridge)
82
+ return respond(id, {
83
+ content: [{ type: 'text', text: typeof result === 'string' ? result : JSON.stringify(result, null, 2) }]
84
+ })
85
+ } catch (e) {
86
+ log(`Tool error [${name}]:`, e.message)
87
+ return respond(id, {
88
+ content: [{ type: 'text', text: `Error: ${e.message}` }],
89
+ isError: true,
90
+ })
142
91
  }
143
-
144
- sendResult(id, { content: [{ type: 'text', text: JSON.stringify(summary, null, 2) }] });
145
- return;
146
92
  }
147
- }
148
-
149
- // ═══ DIRECT FIGMA COMMANDS ═══
150
- // Single commands forwarded directly to plugin
151
- if (relay && relay.isFigmaCommand(toolName) && relay.isConnected()) {
152
- process.stderr.write('CONDUCTOR: -> Figma: ' + toolName + '\n');
153
- try {
154
- var figmaResult = await relay.sendToPlugin(toolName, toolArgs);
155
- process.stderr.write('CONDUCTOR: <- Figma: ' + (figmaResult.name || figmaResult.id || 'ok') + '\n');
156
- sendResult(id, { content: [{ type: 'text', text: JSON.stringify(figmaResult, null, 2) }] });
157
- } catch (err) {
158
- sendResult(id, { content: [{ type: 'text', text: JSON.stringify({ error: String(err) }) }] });
159
- }
160
- return;
161
- }
162
-
163
- // ═══ DESIGN INTELLIGENCE (local) ═══
164
- var result = handleTool(toolName, toolArgs, null);
165
-
166
- // Check if handler produced a Figma action to forward
167
- if (relay && relay.isConnected()) {
168
- try {
169
- var data = JSON.parse(result.content[0].text);
170
- if (data.action && relay.isFigmaCommand(data.action)) {
171
- process.stderr.write('CONDUCTOR: -> Figma (via ' + toolName + '): ' + data.action + '\n');
172
- var fResult = await relay.sendToPlugin(data.action, data);
173
- process.stderr.write('CONDUCTOR: <- Figma: ' + (fResult.name || fResult.id || 'ok') + '\n');
174
- data._figmaResult = fResult;
175
- sendResult(id, { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] });
176
- return;
177
- }
178
- } catch (e) { /* not JSON or no action */ }
179
- }
180
93
 
181
- // Blueprint tool but plugin not connected — return the blueprint spec
182
- if (BLUEPRINT_TOOLS.has(toolName)) {
183
- var bp = getBlueprint(toolName, toolArgs);
184
- if (bp) {
185
- var spec = {
186
- tool: toolName,
187
- description: bp.description,
188
- commandCount: bp.commands.length,
189
- _note: relay && !relay.isConnected()
190
- ? 'Figma plugin not connected. Connect the CONDUCTOR plugin to execute these ' + bp.commands.length + ' commands on canvas.'
191
- : 'WebSocket relay not available. Install ws package for Figma bridge.',
192
- commands: bp.commands.map(function(c, i) { return { step: i, type: c.type, name: c.data.name || c.data.text || '' }; }),
193
- };
194
- sendResult(id, { content: [{ type: 'text', text: JSON.stringify(spec, null, 2) }] });
195
- return;
196
- }
197
- }
94
+ case 'ping':
95
+ return respond(id, {})
198
96
 
199
- // Figma command but not connected — add note
200
- if (relay && relay.isFigmaCommand(toolName) && !relay.isConnected()) {
201
- try {
202
- var parsed = JSON.parse(result.content[0].text);
203
- parsed._note = 'Figma plugin not connected. Connect the CONDUCTOR plugin in Figma to execute on canvas.';
204
- result = { content: [{ type: 'text', text: JSON.stringify(parsed, null, 2) }] };
205
- } catch (e) { /* ignore */ }
97
+ default:
98
+ if (id) respondError(id, -32601, `Unknown method: ${method}`)
206
99
  }
207
-
208
- sendResult(id, result);
209
- }
210
-
211
- function sendResult(id, result) {
212
- process.stdout.write(JSON.stringify({ jsonrpc: '2.0', id: id, result: result }) + '\n');
213
100
  }
214
101
 
215
- function sendError(id, code, message) {
216
- process.stdout.write(JSON.stringify({ jsonrpc: '2.0', id: id, error: { code: code, message: message } }) + '\n');
217
- }
102
+ process.on('SIGINT', () => { if (bridge) bridge.stop(); process.exit(0) })
103
+ process.on('SIGTERM', () => { if (bridge) bridge.stop(); process.exit(0) })