@autobest-ui/agent 1.0.0 → 1.0.2
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 +69 -10
- package/mcp/azurepr-mcp-bridge/azure-devops.js +82 -112
- package/mcp/azurepr-mcp-bridge/index.js +21 -25
- package/mcp/azurepr-mcp-bridge/index.test.js +50 -59
- package/mcp/figma-mcp-bridge/LICENSE +21 -0
- package/mcp/figma-mcp-bridge/README.md +25 -0
- package/mcp/figma-mcp-bridge/config.toml.example +10 -0
- package/mcp/figma-mcp-bridge/index.test.js +47 -0
- package/mcp/figma-mcp-bridge/package.json +16 -0
- package/mcp/figma-mcp-bridge/skills/figma-bridge/SKILL.md +98 -0
- package/mcp/figma-mcp-bridge/src/index.js +68 -0
- package/mcp/figma-mcp-bridge/src/server.js +159 -0
- package/mcp/figma-mcp-bridge/src/tools/context.js +75 -0
- package/mcp/figma-mcp-bridge/src/tools/index.js +2148 -0
- package/mcp/figma-mcp-bridge/src/tools/mutations.js +5829 -0
- package/mcp/figma-mcp-bridge/src/tools/nodes.js +99 -0
- package/mcp/figma-mcp-bridge/src/tools/pages.js +70 -0
- package/mcp/figma-mcp-bridge/src/websocket.js +255 -0
- package/mcp/rag-mcp-bridge/README.md +24 -12
- package/mcp/rag-mcp-bridge/config.toml.example +1 -1
- package/mcp/rag-mcp-bridge/index.js +80 -125
- package/mcp/rag-mcp-bridge/index.test.js +21 -25
- package/package.json +9 -4
- package/plugins/figma-plugin/LICENSE +21 -0
- package/plugins/figma-plugin/README.md +25 -0
- package/plugins/figma-plugin/code.js +6608 -0
- package/plugins/figma-plugin/manifest.json +32 -0
- package/plugins/figma-plugin/scripts/setup.mjs +72 -0
- package/plugins/figma-plugin/scripts/setup.test.mjs +45 -0
- package/plugins/figma-plugin/ui.html +235 -0
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import assert from
|
|
2
|
-
import path from
|
|
3
|
-
import test from
|
|
4
|
-
import { fileURLToPath } from
|
|
5
|
-
import { Client } from
|
|
6
|
-
import { StdioClientTransport } from
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import test from 'node:test';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
6
|
+
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
7
7
|
import {
|
|
8
8
|
extractAddedContent,
|
|
9
9
|
isAttachmentPath,
|
|
@@ -11,66 +11,56 @@ import {
|
|
|
11
11
|
isInsideRequestedPath,
|
|
12
12
|
MAX_CONTENT_LENGTH,
|
|
13
13
|
parsePullRequestLocation,
|
|
14
|
-
serializePullRequest
|
|
15
|
-
} from
|
|
14
|
+
serializePullRequest
|
|
15
|
+
} from './azure-devops.js';
|
|
16
16
|
|
|
17
17
|
const directory = path.dirname(fileURLToPath(import.meta.url));
|
|
18
18
|
|
|
19
|
-
test(
|
|
19
|
+
test('parses supported Azure DevOps PR URLs', () => {
|
|
20
20
|
assert.deepEqual(
|
|
21
|
-
parsePullRequestLocation(
|
|
22
|
-
"https://autobest.visualstudio.com/AutoBestChina/_git/web/pullrequest/42",
|
|
23
|
-
),
|
|
21
|
+
parsePullRequestLocation('https://autobest.visualstudio.com/AutoBestChina/_git/web/pullrequest/42'),
|
|
24
22
|
{
|
|
25
|
-
apiRoot:
|
|
26
|
-
repository:
|
|
27
|
-
pullRequestId:
|
|
28
|
-
}
|
|
23
|
+
apiRoot: 'https://autobest.visualstudio.com/AutoBestChina',
|
|
24
|
+
repository: 'web',
|
|
25
|
+
pullRequestId: '42'
|
|
26
|
+
}
|
|
29
27
|
);
|
|
30
28
|
assert.deepEqual(
|
|
31
|
-
parsePullRequestLocation(
|
|
32
|
-
"https://dev.azure.com/example/My%20Project/_git/front%20end/pullrequest/7",
|
|
33
|
-
),
|
|
29
|
+
parsePullRequestLocation('https://dev.azure.com/example/My%20Project/_git/front%20end/pullrequest/7'),
|
|
34
30
|
{
|
|
35
|
-
apiRoot:
|
|
36
|
-
repository:
|
|
37
|
-
pullRequestId:
|
|
38
|
-
}
|
|
39
|
-
);
|
|
40
|
-
assert.throws(
|
|
41
|
-
() =>
|
|
42
|
-
parsePullRequestLocation(
|
|
43
|
-
"https://example.com/org/project/_git/repo/pullrequest/1",
|
|
44
|
-
),
|
|
45
|
-
/仅支持/,
|
|
31
|
+
apiRoot: 'https://dev.azure.com/example/My%20Project',
|
|
32
|
+
repository: 'front end',
|
|
33
|
+
pullRequestId: '7'
|
|
34
|
+
}
|
|
46
35
|
);
|
|
36
|
+
assert.throws(() => parsePullRequestLocation('https://example.com/org/project/_git/repo/pullrequest/1'), /仅支持/);
|
|
47
37
|
});
|
|
48
38
|
|
|
49
|
-
test(
|
|
50
|
-
assert.equal(isAttachmentPath(
|
|
51
|
-
assert.equal(isBinaryPath(
|
|
52
|
-
assert.equal(isInsideRequestedPath(
|
|
53
|
-
assert.equal(isInsideRequestedPath(
|
|
54
|
-
assert.deepEqual(extractAddedContent(
|
|
55
|
-
content:
|
|
56
|
-
truncated: false
|
|
39
|
+
test('filters paths and extracts added lines', () => {
|
|
40
|
+
assert.equal(isAttachmentPath('/docs/.attachments/image.png'), true);
|
|
41
|
+
assert.equal(isBinaryPath('/assets/logo.PNG'), true);
|
|
42
|
+
assert.equal(isInsideRequestedPath('/src/app/index.js', '/src'), true);
|
|
43
|
+
assert.equal(isInsideRequestedPath('/scripts/build.js', '/src'), false);
|
|
44
|
+
assert.deepEqual(extractAddedContent('a\nb\n', 'a\nc\nb\n'), {
|
|
45
|
+
content: 'c\n',
|
|
46
|
+
truncated: false
|
|
57
47
|
});
|
|
58
48
|
});
|
|
59
49
|
|
|
60
|
-
test(
|
|
50
|
+
test('serializes oversized results as valid bounded JSON', () => {
|
|
61
51
|
const text = serializePullRequest({
|
|
62
|
-
title:
|
|
63
|
-
description:
|
|
64
|
-
status:
|
|
52
|
+
title: 'Large PR',
|
|
53
|
+
description: 'description',
|
|
54
|
+
status: 'active',
|
|
65
55
|
reviewers: [],
|
|
66
56
|
reviewComments: [],
|
|
67
57
|
workItems: [],
|
|
68
58
|
latestIterationId: 1,
|
|
69
59
|
changedFiles: Array.from({ length: 20 }, (_, index) => ({
|
|
70
|
-
changeType:
|
|
60
|
+
changeType: 'edit',
|
|
71
61
|
path: `/src/file-${index}.js`,
|
|
72
|
-
addedContent:
|
|
73
|
-
}))
|
|
62
|
+
addedContent: 'x'.repeat(20_000)
|
|
63
|
+
}))
|
|
74
64
|
});
|
|
75
65
|
|
|
76
66
|
assert.ok(text.length <= MAX_CONTENT_LENGTH);
|
|
@@ -78,35 +68,36 @@ test("serializes oversized results as valid bounded JSON", () => {
|
|
|
78
68
|
assert.ok(JSON.parse(text).addedContentOmittedDueToResponseLimit > 0);
|
|
79
69
|
});
|
|
80
70
|
|
|
81
|
-
test(
|
|
71
|
+
test('initializes and exposes the Azure PR tool over stdio', async () => {
|
|
82
72
|
const client = new Client({
|
|
83
|
-
name:
|
|
84
|
-
version:
|
|
73
|
+
name: 'azurepr-mcp-bridge-test',
|
|
74
|
+
version: '1.0.0'
|
|
85
75
|
});
|
|
86
76
|
const transport = new StdioClientTransport({
|
|
87
77
|
command: process.execPath,
|
|
88
|
-
args: [path.join(directory,
|
|
89
|
-
cwd: path.resolve(directory,
|
|
90
|
-
stderr:
|
|
78
|
+
args: [path.join(directory, 'index.js')],
|
|
79
|
+
cwd: path.resolve(directory, '../..'),
|
|
80
|
+
stderr: 'pipe'
|
|
91
81
|
});
|
|
92
82
|
|
|
93
83
|
try {
|
|
94
84
|
await client.connect(transport);
|
|
95
|
-
assert.equal(client.getServerVersion()?.name,
|
|
96
|
-
assert.match(client.getInstructions() ??
|
|
85
|
+
assert.equal(client.getServerVersion()?.name, 'azurepr-mcp-bridge');
|
|
86
|
+
assert.match(client.getInstructions() ?? '', /AZURE_DEVOPS_PAT/);
|
|
97
87
|
|
|
98
88
|
const { tools } = await client.listTools();
|
|
99
|
-
assert.deepEqual(
|
|
100
|
-
|
|
101
|
-
|
|
89
|
+
assert.deepEqual(
|
|
90
|
+
tools.map(({ name }) => name),
|
|
91
|
+
'get_azure_pull_request'
|
|
92
|
+
);
|
|
102
93
|
assert.equal(tools[0].annotations?.readOnlyHint, true);
|
|
103
94
|
assert.equal(tools[0].annotations?.openWorldHint, true);
|
|
104
95
|
|
|
105
96
|
const missingPatResult = await client.callTool({
|
|
106
|
-
name:
|
|
97
|
+
name: 'get_azure_pull_request',
|
|
107
98
|
arguments: {
|
|
108
|
-
url:
|
|
109
|
-
}
|
|
99
|
+
url: 'https://autobest.visualstudio.com/AutoBestChina/_git/web/pullrequest/42'
|
|
100
|
+
}
|
|
110
101
|
});
|
|
111
102
|
assert.equal(missingPatResult.isError, true);
|
|
112
103
|
assert.match(missingPatResult.content[0].text, /AZURE_DEVOPS_PAT 未配置/);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Magic Spells
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# figma-mcp-bridge
|
|
2
|
+
|
|
3
|
+
`figma-mcp-bridge` 是本地 STDIO MCP 服务。它在 Codex 与 Figma Plugin 之间建立 WebSocket 桥接,使 Agent 可以读取和修改当前打开的 Figma Design 或 FigJam 文档。
|
|
4
|
+
|
|
5
|
+
## 安装与配置
|
|
6
|
+
|
|
7
|
+
先安装 Figma Plugin:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npx --yes --package=@autobest-ui/agent@latest figma-plugin
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
然后将 [config.toml.example](config.toml.example) 中的配置加入 `~/.codex/config.toml`,重启 Codex。Codex 会通过 npm 安装并启动 MCP:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npx --yes --package=@autobest-ui/agent@latest figma-mcp-bridge
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
MCP 使用 stdio 通信,启动后持续等待客户端请求属于正常状态。WebSocket 默认从 `3055` 端口开始;如果端口已被占用,会依次尝试到 `3070`。
|
|
20
|
+
|
|
21
|
+
## 连接 Figma
|
|
22
|
+
|
|
23
|
+
在 Figma 中通过 **Plugins -> Development -> Import plugin from manifest** 导入安装器输出的 `manifest.json`。打开目标文件并运行插件,将插件显示的端口设置为 MCP 实际监听端口。状态显示 `Connected` 后即可使用。
|
|
24
|
+
|
|
25
|
+
生产环境可将 `@latest` 替换为明确版本,以固定 MCP 行为。
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
[mcp_servers.figma-mcp-bridge]
|
|
2
|
+
command = "npx"
|
|
3
|
+
args = ["--yes", "--package=@autobest-ui/agent@latest", "figma-mcp-bridge"]
|
|
4
|
+
startup_timeout_sec = 30
|
|
5
|
+
tool_timeout_sec = 120
|
|
6
|
+
enabled = true
|
|
7
|
+
|
|
8
|
+
[mcp_servers.figma-mcp-bridge.env]
|
|
9
|
+
# 可选。默认从 3055 开始;端口占用时服务会依次尝试到 3070。
|
|
10
|
+
FIGMA_BRIDGE_PORT = "3055"
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import test from 'node:test';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
7
|
+
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
8
|
+
|
|
9
|
+
const directory = path.dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
|
|
11
|
+
test('ships a valid skill frontmatter', async () => {
|
|
12
|
+
const skill = await readFile(path.join(directory, 'skills', 'figma-bridge', 'SKILL.md'), 'utf8');
|
|
13
|
+
assert.match(skill, /^---\nname: figma-bridge\ndescription: [^\n]+\n---\n/);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test('initializes and exposes Figma tools and skill resources over stdio', async () => {
|
|
17
|
+
const client = new Client({
|
|
18
|
+
name: 'figma-mcp-bridge-test',
|
|
19
|
+
version: '1.0.0'
|
|
20
|
+
});
|
|
21
|
+
const transport = new StdioClientTransport({
|
|
22
|
+
command: process.execPath,
|
|
23
|
+
args: [path.join(directory, 'src', 'index.js')],
|
|
24
|
+
cwd: directory,
|
|
25
|
+
env: {
|
|
26
|
+
...process.env,
|
|
27
|
+
FIGMA_BRIDGE_PORT: '0'
|
|
28
|
+
},
|
|
29
|
+
stderr: 'pipe'
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
await client.connect(transport);
|
|
34
|
+
assert.equal(client.getServerVersion()?.name, 'figma-mcp-bridge');
|
|
35
|
+
assert.match(client.getInstructions() ?? '', /Figma MCP Bridge v0\.4\.0/);
|
|
36
|
+
|
|
37
|
+
const { tools } = await client.listTools();
|
|
38
|
+
assert.ok(tools.length >= 90);
|
|
39
|
+
assert.ok(tools.some(({ name }) => name === 'figma_get_context'));
|
|
40
|
+
assert.ok(tools.some(({ name }) => name === 'figma_create_frame'));
|
|
41
|
+
|
|
42
|
+
const { resources } = await client.listResources();
|
|
43
|
+
assert.ok(resources.some(({ uri }) => uri === 'skill://figma-bridge/SKILL.md'));
|
|
44
|
+
} finally {
|
|
45
|
+
await client.close();
|
|
46
|
+
}
|
|
47
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@autobest/figma-mcp-bridge",
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"description": "STDIO MCP server bridging Codex to the Autobest Figma plugin",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "src/index.js",
|
|
8
|
+
"engines": {
|
|
9
|
+
"node": ">=20"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@modelcontextprotocol/sdk": "1.30.0",
|
|
13
|
+
"ws": "8.18.3",
|
|
14
|
+
"zod": "4.4.3"
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: figma-bridge
|
|
3
|
+
description: Operate figma-mcp-bridge for reliable Figma Design and FigJam reads, edits, verification, and export review.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Figma MCP Bridge — Operation Skill
|
|
7
|
+
|
|
8
|
+
How to work through the figma-mcp-bridge effectively and honestly. Read this
|
|
9
|
+
before any write-heavy design session. Applies to bridge **v0.4.0+**.
|
|
10
|
+
|
|
11
|
+
## Verification discipline — the core of this skill
|
|
12
|
+
|
|
13
|
+
**1. Verify bindings by readback, not by assumption.**
|
|
14
|
+
`figma_get_nodes` (full depth) returns `boundVariables` (node-level variable
|
|
15
|
+
bindings), `explicitVariableModes` (pinned modes), `layoutWrap`,
|
|
16
|
+
`counterAxisSpacing`, and `clipsContent`. After binding, pinning, or layout
|
|
17
|
+
work, read the node back and confirm the field says what you intended. Mutating
|
|
18
|
+
tools echo readbacks in their responses (`verified: true`, resulting
|
|
19
|
+
`explicitVariableModes`, per-side stroke weights, etc.) — check them.
|
|
20
|
+
|
|
21
|
+
**2. Export the render and look at it before reporting done.**
|
|
22
|
+
`figma_export_node` writes the image to disk and returns the path — Read the
|
|
23
|
+
file and actually look at it. Property readback cannot see composition: real
|
|
24
|
+
sessions passed every numeric check while renders showed a portrait video well
|
|
25
|
+
where a 16:9 one belonged, a heading breaking mid-word, and a button hugging at
|
|
26
|
+
140px instead of spanning its form. Looking catches what measuring cannot.
|
|
27
|
+
|
|
28
|
+
**3. Report honestly.**
|
|
29
|
+
- If a tool errors, report the failure — never record the work as done.
|
|
30
|
+
- If a readback disagrees with a success response, report that too.
|
|
31
|
+
- Never paper over a missing capability by hardcoding a value.
|
|
32
|
+
- The bridge errors instead of silently no-oping (see error codes below);
|
|
33
|
+
treat those errors as information about Figma's real constraints, not as
|
|
34
|
+
obstacles to retry around.
|
|
35
|
+
|
|
36
|
+
## Errors that mean "Figma forbids this" (don't retry — change approach)
|
|
37
|
+
|
|
38
|
+
- `INSTANCE_SUBLAYER_RESTRICTED` — size binds, resizes, and reorders inside an
|
|
39
|
+
instance are not allowed. Make the change on the component master; it flows
|
|
40
|
+
to every instance.
|
|
41
|
+
- `MODE_NOT_FOUND` / `COLLECTION_NOT_FOUND` — the modeId/collectionId is wrong;
|
|
42
|
+
the error lists valid modes.
|
|
43
|
+
- `WRONG_EDITOR` / `FIGMA_DESIGN_ONLY` — the tool is gated to FigJam or Figma
|
|
44
|
+
Design; the error names the current editor type (there are five: figma,
|
|
45
|
+
figjam, dev, slides, buzz).
|
|
46
|
+
- `BIND_NOT_APPLIED` / `STYLE_NOT_APPLIED` / `RESIZE_NO_OP` /
|
|
47
|
+
`REORDER_FAILED` / `LIMIT_NOT_APPLIED` — the write did not land and the
|
|
48
|
+
bridge is telling you instead of pretending. Report it.
|
|
49
|
+
|
|
50
|
+
## Tool guidance
|
|
51
|
+
|
|
52
|
+
- **Sizing a child to its parent:** use `figma_set_layout_align: STRETCH`, not
|
|
53
|
+
`figma_resize_nodes`. STRETCH preserves width/height variable binds; resize
|
|
54
|
+
may destroy them (the bridge re-applies and warns, but STRETCH avoids the
|
|
55
|
+
problem entirely).
|
|
56
|
+
- **Pinning variable modes:** `figma_set_variable_mode` sets or clears
|
|
57
|
+
(`clear: true`) an explicit mode per collection on nodes *and pages*. Pins
|
|
58
|
+
belong on preview/page frames. Never pin a component master — every instance
|
|
59
|
+
inherits it, per-collection, and the partial correctness hides the fault.
|
|
60
|
+
- **Text styles:** create with `figma_create_text_style`, bind `fontSize` (and
|
|
61
|
+
other text fields) to variables via `figma_set_variable` with `styleId`,
|
|
62
|
+
apply with `figma_apply_style`, delete with `figma_delete_style`.
|
|
63
|
+
- **Borders on one side:** `figma_set_strokes` per-side weights
|
|
64
|
+
(`strokeTopWeight` etc.); `strokes` may be omitted to change weights only.
|
|
65
|
+
A mixed weight reads back as the string `'MIXED'` plus the four per-side
|
|
66
|
+
values.
|
|
67
|
+
- **Min/max sizes:** `figma_set_size_limits`; pass explicit `null` to clear.
|
|
68
|
+
`figma_unbind_variable` on a min/max field also clears the residual literal.
|
|
69
|
+
- **Reordering:** `figma_reorder_node` `position` is the final index among
|
|
70
|
+
siblings (0 = back, `childCount-1` = front), verified by readback.
|
|
71
|
+
- **Hiding:** `figma_set_visible` — don't fake it with opacity 0.
|
|
72
|
+
- **Exports:** the returned `path` is the deliverable; Read it. No need to
|
|
73
|
+
inflate `scale` to force anything.
|
|
74
|
+
- **Constraints:** set `figma_set_constraints` BEFORE converting the parent to
|
|
75
|
+
auto-layout; inside auto-layout parents it's rejected, and it can't be
|
|
76
|
+
overridden on instance sublayers at all.
|
|
77
|
+
|
|
78
|
+
## Auto-layout facts that bite
|
|
79
|
+
|
|
80
|
+
- `SPACE_BETWEEN` is inert when any child has `layoutGrow: 1` (no free space
|
|
81
|
+
to distribute) — `itemSpacing` is ignored then too.
|
|
82
|
+
- `figma_set_layout_align: CENTER` is a no-op on a stretched child — use
|
|
83
|
+
`INHERIT` plus the parent's `counterAxisAlignItems: CENTER`.
|
|
84
|
+
- Reading wrap: `layoutWrap` and `counterAxisSpacing` come back in full node
|
|
85
|
+
reads; compact children include x/y for geometry checks.
|
|
86
|
+
- `figma_set_text` does not decode HTML entities — send literal characters
|
|
87
|
+
(`&`, not `&`).
|
|
88
|
+
|
|
89
|
+
## Concurrency (multi-agent sessions)
|
|
90
|
+
|
|
91
|
+
- One WebSocket to one open document. More than ~2 write-heavy agents produces
|
|
92
|
+
transient `Unable to establish connection` errors — retry them; if an agent
|
|
93
|
+
stalls completely, kill and restart it.
|
|
94
|
+
- **Never call `figma_set_current_page` or `figma_set_selection` from a
|
|
95
|
+
sub-agent** — it yanks the shared view out from under every other client.
|
|
96
|
+
- The plugin must be connected to THIS server's port: check
|
|
97
|
+
`figma_server_info` / `figma_get_context` first and surface the port to the
|
|
98
|
+
user if disconnected.
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Figma MCP Bridge - Entry Point
|
|
5
|
+
*
|
|
6
|
+
* Starts the WebSocket server for Figma plugin communication
|
|
7
|
+
* and the MCP server for Agent communication.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
11
|
+
import { FigmaBridge } from './websocket.js';
|
|
12
|
+
import { createServer } from './server.js';
|
|
13
|
+
|
|
14
|
+
const PORT = parseInt(process.env.FIGMA_BRIDGE_PORT || '3055', 10);
|
|
15
|
+
|
|
16
|
+
async function main() {
|
|
17
|
+
// eslint-disable-next-line no-console
|
|
18
|
+
console.error('[FigmaMCP] Starting Figma MCP Bridge...');
|
|
19
|
+
|
|
20
|
+
// Create and start WebSocket bridge
|
|
21
|
+
const bridge = new FigmaBridge(PORT);
|
|
22
|
+
await bridge.start();
|
|
23
|
+
|
|
24
|
+
// Log connection events
|
|
25
|
+
bridge.on('connected', info => {
|
|
26
|
+
// eslint-disable-next-line no-console
|
|
27
|
+
console.error(`[FigmaMCP] Figma connected: ${info.fileName}`);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
bridge.on('disconnected', () => {
|
|
31
|
+
// eslint-disable-next-line no-console
|
|
32
|
+
console.error('[FigmaMCP] Figma disconnected');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// Create MCP server
|
|
36
|
+
const server = createServer(bridge);
|
|
37
|
+
|
|
38
|
+
// Connect to stdio transport (Agent communication)
|
|
39
|
+
const transport = new StdioServerTransport();
|
|
40
|
+
await server.connect(transport);
|
|
41
|
+
|
|
42
|
+
// eslint-disable-next-line no-console
|
|
43
|
+
console.error(
|
|
44
|
+
`[FigmaMCP] MCP server running, WebSocket bound to port ${bridge.port}. Waiting for Figma plugin connection...`
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
// Graceful shutdown helper
|
|
48
|
+
const shutdown = async reason => {
|
|
49
|
+
// eslint-disable-next-line no-console
|
|
50
|
+
console.error(`[FigmaMCP] Shutting down (${reason})...`);
|
|
51
|
+
await bridge.stop();
|
|
52
|
+
process.exit(0);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// Handle graceful shutdown
|
|
56
|
+
process.on('SIGINT', () => shutdown('SIGINT'));
|
|
57
|
+
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
58
|
+
|
|
59
|
+
// Handle stdio close (when the Agent closes the connection)
|
|
60
|
+
process.stdin.on('close', () => shutdown('stdin closed'));
|
|
61
|
+
transport.onclose = () => shutdown('transport closed');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
main().catch(error => {
|
|
65
|
+
// eslint-disable-next-line no-console
|
|
66
|
+
console.error('[FigmaMCP] Fatal error:', error);
|
|
67
|
+
process.exit(1);
|
|
68
|
+
});
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Server setup
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { readFileSync, readdirSync, existsSync } from 'fs';
|
|
6
|
+
import { fileURLToPath } from 'url';
|
|
7
|
+
import { dirname, join } from 'path';
|
|
8
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
9
|
+
import { registerTools } from './tools/index.js';
|
|
10
|
+
|
|
11
|
+
// Read package.json once at module load so version stays in sync with the published artifact
|
|
12
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
let pkgVersion = '0.0.0';
|
|
14
|
+
try {
|
|
15
|
+
const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8'));
|
|
16
|
+
pkgVersion = pkg.version;
|
|
17
|
+
} catch (_) {
|
|
18
|
+
// Fall back to placeholder if package.json can't be read
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Create and configure the MCP server
|
|
23
|
+
* @param {FigmaBridge} bridge - Figma bridge instance (must be started — bridge.port must reflect the bound port)
|
|
24
|
+
* @returns {McpServer} Configured MCP server
|
|
25
|
+
*/
|
|
26
|
+
export function createServer(bridge) {
|
|
27
|
+
const port = bridge.port;
|
|
28
|
+
const instructions = `# Figma MCP Bridge v${pkgVersion}
|
|
29
|
+
|
|
30
|
+
## CONNECTION INFO — CHECK FIRST
|
|
31
|
+
|
|
32
|
+
This MCP server is bridging an Agent to Figma via a WebSocket on **port ${port}**.
|
|
33
|
+
|
|
34
|
+
Multiple Agent sessions can run concurrently and the bridge falls back through ports 3055–3070, so the port may differ from the default. **At the start of any new Figma-related conversation, call \`figma_get_context\` to check connection state. If it returns \`connected: false\`, proactively tell the user:**
|
|
35
|
+
|
|
36
|
+
> "The Figma MCP bridge is running on port **${port}**. Open the Figma plugin and set its port input to **${port}**, then re-run the plugin if it was already open."
|
|
37
|
+
|
|
38
|
+
Don't make the user discover the port themselves — surface it the first time you notice they aren't connected.
|
|
39
|
+
|
|
40
|
+
## SKILLS — READ BEFORE WRITE-HEAVY WORK
|
|
41
|
+
|
|
42
|
+
This server ships its own skills as MCP resources. **Before any session that creates or edits design content (not just single reads), read \`skill://figma-bridge/SKILL.md\`** — it covers verification discipline (readback + export-and-look), the error codes that mean "Figma forbids this", bind-preserving sizing, mode pinning, concurrency rules, and auto-layout traps. List resources to discover any additional skills shipped with this server version.
|
|
43
|
+
|
|
44
|
+
## FigJam Support
|
|
45
|
+
|
|
46
|
+
This server supports both Figma design files AND FigJam files. FigJam-specific tools (sticky notes, flowchart shapes, connectors, tables, code blocks, link previews) are gated to FigJam files and return a \`WRONG_EDITOR\` error if called against a Figma design file.
|
|
47
|
+
|
|
48
|
+
**Editor-restricted tools:**
|
|
49
|
+
- FigJam-only (return \`WRONG_EDITOR\` in design files): all sticky / shape-with-text / connector / table / code-block / link-preview tools
|
|
50
|
+
- Figma Design only (return \`FIGMA_DESIGN_ONLY\` in FigJam): \`figma_create_page\`, \`figma_duplicate_page\`. FigJam files have pages but the plugin API does not expose page creation; pages must be created via the FigJam UI by the user.
|
|
51
|
+
|
|
52
|
+
For flowcharts in FigJam:
|
|
53
|
+
- \`figma_create_shape_with_text\` with \`shapeType\` (ROUNDED_RECTANGLE for processes, DIAMOND for decisions, ENG_DATABASE for data stores, etc.)
|
|
54
|
+
- \`figma_create_connector\` with \`{ start: { nodeId, magnet: 'AUTO' }, end: { nodeId, magnet: 'AUTO' } }\` — \`endCap\` defaults to \`ARROW_EQUILATERAL\` so connectors look like arrows
|
|
55
|
+
- Wrap the diagram in a \`figma_create_section\` for grouping
|
|
56
|
+
|
|
57
|
+
## IMPORTANT: Always Use Search Tools First
|
|
58
|
+
|
|
59
|
+
When working with Figma documents, ALWAYS prefer search tools over bulk retrieval:
|
|
60
|
+
|
|
61
|
+
### For Variables
|
|
62
|
+
- **USE**: \`figma_search_variables\` (~500 tokens) - Filter by name pattern, type, collection
|
|
63
|
+
- **AVOID**: \`figma_get_local_variables\` (25k+ tokens, may truncate)
|
|
64
|
+
|
|
65
|
+
Example:
|
|
66
|
+
\`\`\`
|
|
67
|
+
figma_search_variables({ namePattern: "colors/*", type: "COLOR", compact: true })
|
|
68
|
+
\`\`\`
|
|
69
|
+
|
|
70
|
+
### For Nodes
|
|
71
|
+
- **USE**: \`figma_search_nodes\` - Find frames/elements by name within a scope
|
|
72
|
+
- **USE**: \`figma_get_children\` - Browse hierarchy one level at a time
|
|
73
|
+
- **AVOID**: Repeated \`figma_get_nodes\` calls to traverse the tree
|
|
74
|
+
|
|
75
|
+
Example:
|
|
76
|
+
\`\`\`
|
|
77
|
+
figma_search_nodes({ parentId: "0:1", nameContains: "Button", types: ["FRAME", "COMPONENT"] })
|
|
78
|
+
\`\`\`
|
|
79
|
+
|
|
80
|
+
### For Components
|
|
81
|
+
- **USE**: \`figma_search_components\` - Find by name pattern
|
|
82
|
+
- Returns compact results with component metadata
|
|
83
|
+
|
|
84
|
+
### For Styles
|
|
85
|
+
- **USE**: \`figma_search_styles\` - Find by name and type
|
|
86
|
+
- **AVOID**: \`figma_get_local_styles\` for large documents
|
|
87
|
+
|
|
88
|
+
## Workflow
|
|
89
|
+
|
|
90
|
+
1. **Start with context**: Call \`figma_get_context\` to understand the current document and selection
|
|
91
|
+
2. **Search first**: Use search tools to find specific elements by name
|
|
92
|
+
3. **Get details only when needed**: Use \`figma_get_nodes\` with \`depth: "minimal"\` or \`"compact"\` for efficiency
|
|
93
|
+
4. **Use full depth sparingly**: Only use \`depth: "full"\` when you need all node properties
|
|
94
|
+
|
|
95
|
+
## Token Optimization
|
|
96
|
+
|
|
97
|
+
| Tool | Tokens | Use Case |
|
|
98
|
+
|------|--------|----------|
|
|
99
|
+
| \`figma_search_*\` | ~50/result | Finding specific elements |
|
|
100
|
+
| \`figma_get_children\` | ~50/node | Browsing hierarchy |
|
|
101
|
+
| \`figma_get_nodes\` (minimal) | ~100/node | Tree traversal |
|
|
102
|
+
| \`figma_get_nodes\` (full) | ~500/node | Detailed inspection |
|
|
103
|
+
| \`figma_get_local_variables\` | 25k+ | AVOID - use search instead
|
|
104
|
+
`;
|
|
105
|
+
const server = new McpServer(
|
|
106
|
+
{
|
|
107
|
+
name: 'figma-mcp-bridge',
|
|
108
|
+
version: pkgVersion
|
|
109
|
+
},
|
|
110
|
+
{ instructions }
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
// Register all Figma tools
|
|
114
|
+
registerTools(server, bridge);
|
|
115
|
+
|
|
116
|
+
// Serve every skills/<name>/SKILL.md as an MCP resource (skill://<name>/SKILL.md)
|
|
117
|
+
// so agents get the bridge's operating knowledge without installing anything.
|
|
118
|
+
registerSkillResources(server);
|
|
119
|
+
|
|
120
|
+
return server;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Register the markdown skills shipped in skills/ as MCP resources.
|
|
125
|
+
* Files are read lazily per request so a dev checkout picks up edits
|
|
126
|
+
* without a server restart. Missing dir (or a race on a deleted file)
|
|
127
|
+
* degrades to no/absent resources rather than a crash.
|
|
128
|
+
* @param {McpServer} server
|
|
129
|
+
*/
|
|
130
|
+
function registerSkillResources(server) {
|
|
131
|
+
const skillsDir = join(__dirname, '..', 'skills');
|
|
132
|
+
if (!existsSync(skillsDir)) return;
|
|
133
|
+
|
|
134
|
+
for (const entry of readdirSync(skillsDir, { withFileTypes: true })) {
|
|
135
|
+
if (!entry.isDirectory()) continue;
|
|
136
|
+
const skillPath = join(skillsDir, entry.name, 'SKILL.md');
|
|
137
|
+
if (!existsSync(skillPath)) continue;
|
|
138
|
+
|
|
139
|
+
const uri = `skill://${entry.name}/SKILL.md`;
|
|
140
|
+
server.registerResource(
|
|
141
|
+
entry.name,
|
|
142
|
+
uri,
|
|
143
|
+
{
|
|
144
|
+
title: `Skill: ${entry.name}`,
|
|
145
|
+
description: `Operating skill shipped with figma-mcp-bridge. Read before write-heavy ${entry.name} work.`,
|
|
146
|
+
mimeType: 'text/markdown'
|
|
147
|
+
},
|
|
148
|
+
async () => ({
|
|
149
|
+
contents: [
|
|
150
|
+
{
|
|
151
|
+
uri,
|
|
152
|
+
mimeType: 'text/markdown',
|
|
153
|
+
text: readFileSync(skillPath, 'utf8')
|
|
154
|
+
}
|
|
155
|
+
]
|
|
156
|
+
})
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
}
|