conductor-figma 3.0.1 → 3.0.3
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 +0 -16
- package/package.json +1 -1
- package/src/bridge.js +52 -36
- package/src/server.js +59 -45
package/README.md
CHANGED
|
@@ -80,22 +80,6 @@ Every tool queries the intelligence engine:
|
|
|
80
80
|
- **Font Weights**: Auto-resolves "bold" → "Bold", "600" → "Semi Bold"
|
|
81
81
|
- **Layout Intelligence**: 13 layout presets (row, column, center, spread, grid, sidebar, form)
|
|
82
82
|
|
|
83
|
-
## vs Competition
|
|
84
|
-
|
|
85
|
-
|---------|-----------|--------|-----------|---------------|
|
|
86
|
-
| Tools | 201 | 45 | 5 | 12 |
|
|
87
|
-
| Categories | 17 | ~8 | 1 | ~4 |
|
|
88
|
-
| Design intelligence | Yes | No | No | No |
|
|
89
|
-
| Smart components | 18 types | No | No | No |
|
|
90
|
-
| Accessibility | 12 tools | No | No | No |
|
|
91
|
-
| Code export | 5 frameworks | No | No | Partial |
|
|
92
|
-
| Batch operations | 17 tools | No | No | No |
|
|
93
|
-
| Design system | 10 tools (free) | $9 add-on | No | Partial |
|
|
94
|
-
| Responsive | 5 tools | No | No | No |
|
|
95
|
-
| Typography | 10 tools | No | No | No |
|
|
96
|
-
| Prototype | 10 tools | No | No | No |
|
|
97
|
-
| Annotation | 10 tools | No | No | No |
|
|
98
|
-
|
|
99
83
|
## License
|
|
100
84
|
|
|
101
85
|
MIT. Built by [0xDragoon](https://github.com/dragoon0x).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "conductor-figma",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.3",
|
|
4
4
|
"description": "Design-intelligent MCP server for Figma. 201 design-intelligent tools for Figma. Every tool knows typography, spacing, color, accessibility. Not a shape proxy — a design engine.",
|
|
5
5
|
"author": "0xDragoon",
|
|
6
6
|
"license": "MIT",
|
package/src/bridge.js
CHANGED
|
@@ -4,57 +4,73 @@
|
|
|
4
4
|
|
|
5
5
|
import { WebSocketServer } from 'ws'
|
|
6
6
|
|
|
7
|
-
function log(
|
|
7
|
+
function log() { process.stderr.write('[bridge] ' + Array.prototype.join.call(arguments, ' ') + '\n') }
|
|
8
8
|
|
|
9
9
|
export function createBridge(port) {
|
|
10
10
|
port = port || parseInt(process.env.CONDUCTOR_PORT) || 3055
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
var wss = null
|
|
12
|
+
var client = null
|
|
13
|
+
var pending = new Map()
|
|
14
|
+
var msgId = 0
|
|
15
|
+
var isOwner = false
|
|
15
16
|
|
|
16
17
|
function start() {
|
|
17
|
-
return new Promise((resolve)
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
18
|
+
return new Promise(function(resolve) {
|
|
19
|
+
try {
|
|
20
|
+
wss = new WebSocketServer({ port: port })
|
|
21
|
+
wss.on('listening', function() { isOwner = true; log('WebSocket server on port ' + port); resolve() })
|
|
22
|
+
wss.on('connection', function(ws) {
|
|
23
|
+
client = ws
|
|
24
|
+
log('Figma plugin connected')
|
|
25
|
+
ws.on('message', function(data) {
|
|
26
|
+
try {
|
|
27
|
+
var msg = JSON.parse(data.toString())
|
|
28
|
+
if (msg.id && pending.has(msg.id)) {
|
|
29
|
+
var p = pending.get(msg.id)
|
|
30
|
+
clearTimeout(p.timer)
|
|
31
|
+
pending.delete(msg.id)
|
|
32
|
+
if (msg.error) p.reject(new Error(msg.error))
|
|
33
|
+
else p.resolve(msg.result)
|
|
34
|
+
}
|
|
35
|
+
} catch (e) { log('Parse error:', e.message) }
|
|
36
|
+
})
|
|
37
|
+
ws.on('close', function() { client = null; log('Figma plugin disconnected') })
|
|
38
|
+
ws.on('error', function(e) { log('WS error:', e.message) })
|
|
34
39
|
})
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
40
|
+
wss.on('error', function(e) {
|
|
41
|
+
if (e.code === 'EADDRINUSE') {
|
|
42
|
+
log('Port ' + port + ' in use — another Conductor instance is running. This instance will handle MCP only.')
|
|
43
|
+
wss = null
|
|
44
|
+
resolve()
|
|
45
|
+
} else {
|
|
46
|
+
log('Server error:', e.message)
|
|
47
|
+
resolve()
|
|
48
|
+
}
|
|
49
|
+
})
|
|
50
|
+
} catch (e) {
|
|
51
|
+
log('Bridge start error:', e.message)
|
|
52
|
+
resolve()
|
|
53
|
+
}
|
|
39
54
|
})
|
|
40
55
|
}
|
|
41
56
|
|
|
42
|
-
function stop() { if (wss) { wss.close(); wss = null } }
|
|
57
|
+
function stop() { if (wss && isOwner) { wss.close(); wss = null } }
|
|
43
58
|
|
|
44
59
|
function isConnected() { return client !== null && client.readyState === 1 }
|
|
45
60
|
|
|
46
|
-
function send(command, data, timeout
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
61
|
+
function send(command, data, timeout) {
|
|
62
|
+
timeout = timeout || 15000
|
|
63
|
+
return new Promise(function(resolve, reject) {
|
|
64
|
+
if (!isConnected()) return reject(new Error('Figma plugin not connected. Run the Conductor plugin in Figma, and make sure the WebSocket server is running on port ' + port))
|
|
65
|
+
var id = ++msgId
|
|
66
|
+
var timer = setTimeout(function() {
|
|
51
67
|
pending.delete(id)
|
|
52
|
-
reject(new Error(
|
|
68
|
+
reject(new Error('Timeout waiting for Figma response (' + timeout + 'ms)'))
|
|
53
69
|
}, timeout)
|
|
54
|
-
pending.set(id, { resolve, reject, timer })
|
|
55
|
-
client.send(JSON.stringify({ id, command, data }))
|
|
70
|
+
pending.set(id, { resolve: resolve, reject: reject, timer: timer })
|
|
71
|
+
client.send(JSON.stringify({ id: id, command: command, data: data }))
|
|
56
72
|
})
|
|
57
73
|
}
|
|
58
74
|
|
|
59
|
-
return { start, stop, isConnected, send, getPort: ()
|
|
75
|
+
return { start: start, stop: stop, isConnected: isConnected, send: send, getPort: function() { return port } }
|
|
60
76
|
}
|
package/src/server.js
CHANGED
|
@@ -6,96 +6,110 @@ import { TOOL_LIST, TOOL_COUNT, getTool, CATEGORIES } from './tools/registry.js'
|
|
|
6
6
|
import { handleTool } from './tools/handlers.js'
|
|
7
7
|
import { createBridge } from './bridge.js'
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
var VERSION = '3.0.2'
|
|
10
|
+
var bridge = null
|
|
11
|
+
var bridgeStarted = false
|
|
12
12
|
|
|
13
|
-
function log(
|
|
14
|
-
|
|
13
|
+
function log() { process.stderr.write('[conductor] ' + Array.prototype.join.call(arguments, ' ') + '\n') }
|
|
14
|
+
|
|
15
|
+
function ensureBridge() {
|
|
16
|
+
if (!bridgeStarted) {
|
|
17
|
+
bridgeStarted = true
|
|
18
|
+
bridge = createBridge()
|
|
19
|
+
bridge.start()
|
|
20
|
+
}
|
|
21
|
+
return bridge
|
|
22
|
+
}
|
|
15
23
|
|
|
16
24
|
// ─── JSON-RPC over stdio ───
|
|
17
|
-
|
|
25
|
+
var buffer = ''
|
|
18
26
|
|
|
19
27
|
process.stdin.setEncoding('utf8')
|
|
20
|
-
process.stdin.on('data', chunk
|
|
28
|
+
process.stdin.on('data', function(chunk) {
|
|
21
29
|
buffer += chunk
|
|
22
30
|
while (true) {
|
|
23
|
-
|
|
31
|
+
var headerEnd = buffer.indexOf('\r\n\r\n')
|
|
24
32
|
if (headerEnd === -1) break
|
|
25
|
-
|
|
26
|
-
|
|
33
|
+
var header = buffer.slice(0, headerEnd)
|
|
34
|
+
var match = header.match(/Content-Length:\s*(\d+)/i)
|
|
27
35
|
if (!match) { buffer = buffer.slice(headerEnd + 4); continue }
|
|
28
|
-
|
|
29
|
-
|
|
36
|
+
var len = parseInt(match[1])
|
|
37
|
+
var bodyStart = headerEnd + 4
|
|
30
38
|
if (buffer.length < bodyStart + len) break
|
|
31
|
-
|
|
39
|
+
var body = buffer.slice(bodyStart, bodyStart + len)
|
|
32
40
|
buffer = buffer.slice(bodyStart + len)
|
|
33
41
|
try {
|
|
34
|
-
|
|
42
|
+
var msg = JSON.parse(body)
|
|
35
43
|
handleMessage(msg)
|
|
36
44
|
} catch (e) { log('Parse error:', e.message) }
|
|
37
45
|
}
|
|
38
46
|
})
|
|
39
47
|
|
|
40
48
|
function send(msg) {
|
|
41
|
-
|
|
42
|
-
|
|
49
|
+
var json = JSON.stringify(msg)
|
|
50
|
+
var out = 'Content-Length: ' + Buffer.byteLength(json) + '\r\n\r\n' + json
|
|
43
51
|
process.stdout.write(out)
|
|
44
52
|
}
|
|
45
53
|
|
|
46
|
-
function respond(id, result) { send({ jsonrpc: '2.0', id, result }) }
|
|
47
|
-
function respondError(id, code, message) { send({ jsonrpc: '2.0', id, error: { code, message } }) }
|
|
54
|
+
function respond(id, result) { send({ jsonrpc: '2.0', id: id, result: result }) }
|
|
55
|
+
function respondError(id, code, message) { send({ jsonrpc: '2.0', id: id, error: { code: code, message: message } }) }
|
|
48
56
|
|
|
49
|
-
|
|
50
|
-
|
|
57
|
+
function handleMessage(msg) {
|
|
58
|
+
var id = msg.id
|
|
59
|
+
var method = msg.method
|
|
60
|
+
var params = msg.params
|
|
51
61
|
|
|
52
62
|
switch (method) {
|
|
53
63
|
case 'initialize':
|
|
54
|
-
|
|
64
|
+
// Start bridge in background, respond immediately
|
|
65
|
+
ensureBridge()
|
|
66
|
+
log('Conductor v' + VERSION + ' ready — ' + TOOL_COUNT + ' tools')
|
|
67
|
+
respond(id, {
|
|
55
68
|
protocolVersion: '2024-11-05',
|
|
56
69
|
capabilities: { tools: { listChanged: false } },
|
|
57
70
|
serverInfo: { name: 'conductor-figma', version: VERSION },
|
|
58
71
|
})
|
|
72
|
+
return
|
|
59
73
|
|
|
60
74
|
case 'notifications/initialized':
|
|
61
|
-
log(`Conductor v${VERSION} ready — ${TOOL_COUNT} tools across ${Object.keys(CATEGORIES).length} categories`)
|
|
62
75
|
return
|
|
63
76
|
|
|
64
77
|
case 'tools/list':
|
|
65
|
-
|
|
66
|
-
tools: TOOL_LIST.map(t
|
|
67
|
-
name: t.name,
|
|
68
|
-
|
|
69
|
-
inputSchema: t.inputSchema,
|
|
70
|
-
}))
|
|
78
|
+
respond(id, {
|
|
79
|
+
tools: TOOL_LIST.map(function(t) {
|
|
80
|
+
return { name: t.name, description: t.description, inputSchema: t.inputSchema }
|
|
81
|
+
})
|
|
71
82
|
})
|
|
83
|
+
return
|
|
72
84
|
|
|
73
|
-
case 'tools/call':
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
85
|
+
case 'tools/call':
|
|
86
|
+
var name = params.name
|
|
87
|
+
var args = params.arguments || {}
|
|
88
|
+
var tool = getTool(name)
|
|
89
|
+
if (!tool) { respondError(id, -32601, 'Unknown tool: ' + name); return }
|
|
77
90
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
91
|
+
var b = ensureBridge()
|
|
92
|
+
handleTool(name, args, b).then(function(result) {
|
|
93
|
+
respond(id, {
|
|
81
94
|
content: [{ type: 'text', text: typeof result === 'string' ? result : JSON.stringify(result, null, 2) }]
|
|
82
95
|
})
|
|
83
|
-
}
|
|
84
|
-
log(
|
|
85
|
-
|
|
86
|
-
content: [{ type: 'text', text:
|
|
96
|
+
}).catch(function(e) {
|
|
97
|
+
log('Tool error [' + name + ']:', e.message)
|
|
98
|
+
respond(id, {
|
|
99
|
+
content: [{ type: 'text', text: 'Error: ' + e.message }],
|
|
87
100
|
isError: true,
|
|
88
101
|
})
|
|
89
|
-
}
|
|
90
|
-
|
|
102
|
+
})
|
|
103
|
+
return
|
|
91
104
|
|
|
92
105
|
case 'ping':
|
|
93
|
-
|
|
106
|
+
respond(id, {})
|
|
107
|
+
return
|
|
94
108
|
|
|
95
109
|
default:
|
|
96
|
-
if (id) respondError(id, -32601,
|
|
110
|
+
if (id) respondError(id, -32601, 'Unknown method: ' + method)
|
|
97
111
|
}
|
|
98
112
|
}
|
|
99
113
|
|
|
100
|
-
process.on('SIGINT', ()
|
|
101
|
-
process.on('SIGTERM', ()
|
|
114
|
+
process.on('SIGINT', function() { if (bridge) bridge.stop(); process.exit(0) })
|
|
115
|
+
process.on('SIGTERM', function() { if (bridge) bridge.stop(); process.exit(0) })
|