@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.
Files changed (30) hide show
  1. package/README.md +69 -10
  2. package/mcp/azurepr-mcp-bridge/azure-devops.js +82 -112
  3. package/mcp/azurepr-mcp-bridge/index.js +21 -25
  4. package/mcp/azurepr-mcp-bridge/index.test.js +50 -59
  5. package/mcp/figma-mcp-bridge/LICENSE +21 -0
  6. package/mcp/figma-mcp-bridge/README.md +25 -0
  7. package/mcp/figma-mcp-bridge/config.toml.example +10 -0
  8. package/mcp/figma-mcp-bridge/index.test.js +47 -0
  9. package/mcp/figma-mcp-bridge/package.json +16 -0
  10. package/mcp/figma-mcp-bridge/skills/figma-bridge/SKILL.md +98 -0
  11. package/mcp/figma-mcp-bridge/src/index.js +68 -0
  12. package/mcp/figma-mcp-bridge/src/server.js +159 -0
  13. package/mcp/figma-mcp-bridge/src/tools/context.js +75 -0
  14. package/mcp/figma-mcp-bridge/src/tools/index.js +2148 -0
  15. package/mcp/figma-mcp-bridge/src/tools/mutations.js +5829 -0
  16. package/mcp/figma-mcp-bridge/src/tools/nodes.js +99 -0
  17. package/mcp/figma-mcp-bridge/src/tools/pages.js +70 -0
  18. package/mcp/figma-mcp-bridge/src/websocket.js +255 -0
  19. package/mcp/rag-mcp-bridge/README.md +24 -12
  20. package/mcp/rag-mcp-bridge/config.toml.example +1 -1
  21. package/mcp/rag-mcp-bridge/index.js +80 -125
  22. package/mcp/rag-mcp-bridge/index.test.js +21 -25
  23. package/package.json +9 -4
  24. package/plugins/figma-plugin/LICENSE +21 -0
  25. package/plugins/figma-plugin/README.md +25 -0
  26. package/plugins/figma-plugin/code.js +6608 -0
  27. package/plugins/figma-plugin/manifest.json +32 -0
  28. package/plugins/figma-plugin/scripts/setup.mjs +72 -0
  29. package/plugins/figma-plugin/scripts/setup.test.mjs +45 -0
  30. package/plugins/figma-plugin/ui.html +235 -0
@@ -1,9 +1,9 @@
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";
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 "./azure-devops.js";
14
+ serializePullRequest
15
+ } from './azure-devops.js';
16
16
 
17
17
  const directory = path.dirname(fileURLToPath(import.meta.url));
18
18
 
19
- test("parses supported Azure DevOps PR URLs", () => {
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: "https://autobest.visualstudio.com/AutoBestChina",
26
- repository: "web",
27
- pullRequestId: "42",
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: "https://dev.azure.com/example/My%20Project",
36
- repository: "front end",
37
- pullRequestId: "7",
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("filters paths and extracts added lines", () => {
50
- assert.equal(isAttachmentPath("/docs/.attachments/image.png"), true);
51
- assert.equal(isBinaryPath("/assets/logo.PNG"), true);
52
- assert.equal(isInsideRequestedPath("/src/app/index.js", "/src"), true);
53
- assert.equal(isInsideRequestedPath("/scripts/build.js", "/src"), false);
54
- assert.deepEqual(extractAddedContent("a\nb\n", "a\nc\nb\n"), {
55
- content: "c\n",
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("serializes oversized results as valid bounded JSON", () => {
50
+ test('serializes oversized results as valid bounded JSON', () => {
61
51
  const text = serializePullRequest({
62
- title: "Large PR",
63
- description: "description",
64
- status: "active",
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: "edit",
60
+ changeType: 'edit',
71
61
  path: `/src/file-${index}.js`,
72
- addedContent: "x".repeat(20_000),
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("initializes and exposes the Azure PR tool over stdio", async () => {
71
+ test('initializes and exposes the Azure PR tool over stdio', async () => {
82
72
  const client = new Client({
83
- name: "azurepr-mcp-bridge-test",
84
- version: "1.0.0",
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, "index.js")],
89
- cwd: path.resolve(directory, "../.."),
90
- stderr: "pipe",
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, "azurepr-mcp-bridge");
96
- assert.match(client.getInstructions() ?? "", /AZURE_DEVOPS_PAT/);
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(tools.map(({ name }) => name), [
100
- "get_azure_pull_request",
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: "get_azure_pull_request",
97
+ name: 'get_azure_pull_request',
107
98
  arguments: {
108
- url: "https://autobest.visualstudio.com/AutoBestChina/_git/web/pullrequest/42",
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 `&amp;`).
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
+ }