@browxai/plugin-figma 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Kalebtec
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.
package/README.md ADDED
@@ -0,0 +1,41 @@
1
+ # @browxai/plugin-figma
2
+
3
+ First-party browxai canvas-app adapter for Figma. Exposes five small,
4
+ useful tools (`figma.get_selection`, `figma.get_viewport`,
5
+ `figma.select_node`, `figma.move_node`, `figma.create_rectangle`) over the
6
+ page-side `figma.*` global that Figma's plugin context exposes. Each tool
7
+ is a thin wrapper around an `eval_js` round-trip: the plugin builds the
8
+ appropriate `figma.viewport` / `figma.currentPage.selection` /
9
+ `figma.createRectangle()` expression, dispatches through `eval_js`, and
10
+ parses the value back. When `figma` isn't defined on the page (no editor
11
+ loaded), every tool returns the structured `code:"figma-not-loaded"`
12
+ envelope rather than crashing.
13
+
14
+ ## Install
15
+
16
+ ```sh
17
+ $ browxai plugin install @browxai/plugin-figma
18
+ ```
19
+
20
+ The host must have the `eval` and `canvas` capabilities enabled — the
21
+ plugin declares both at the manifest level and the runtime gates the
22
+ whole plugin against the operator's active capability set.
23
+
24
+ After install, restart the browxai server (plugin lifecycle is
25
+ resolved-once-at-server-start). The tools surface as
26
+ `figma.get_selection` (etc.) on MCP `tools/list`, and on the SDK as
27
+ `client.plugins.figma.get_selection(...)`.
28
+
29
+ ## Targeted Figma API surface
30
+
31
+ This plugin pokes the long-stable parts of the Figma plugin API as of
32
+ 2026-06: `figma.viewport.{center,zoom}`, `figma.currentPage.selection`,
33
+ `figma.getNodeById()`, `figma.createRectangle()`, plus mutable `x` / `y`
34
+ / `fills` properties on scene nodes. Future Figma versions may add
35
+ fields; the targeted surface should remain compatible.
36
+
37
+ ## Full reference
38
+
39
+ The per-tool reference for this adapter — every op with args, return
40
+ shape, and error codes, plus a usage walkthrough — lives at
41
+ <https://browxai.com/plugins/first-party/>.
@@ -0,0 +1,38 @@
1
+ interface ToolResponse {
2
+ readonly content: ReadonlyArray<{
3
+ type: "text";
4
+ text: string;
5
+ } | {
6
+ type: "image";
7
+ data: string;
8
+ mimeType: string;
9
+ }>;
10
+ }
11
+ interface PluginApi {
12
+ readonly namespace: string;
13
+ readonly declaredCapabilities: ReadonlyArray<string>;
14
+ registerTool(name: string, def: {
15
+ description: string;
16
+ inputSchema?: Record<string, any> | undefined;
17
+ }, handler: (args: unknown) => Promise<ToolResponse>): void;
18
+ callTool(name: string, args?: Record<string, unknown>): Promise<ToolResponse>;
19
+ log: {
20
+ info(msg: string, meta?: Record<string, unknown>): void;
21
+ warn(msg: string, meta?: Record<string, unknown>): void;
22
+ error(msg: string, meta?: Record<string, unknown>): void;
23
+ };
24
+ }
25
+ export declare const handlers: {
26
+ /** `figma.get_selection()` → `{ok, nodes:[{id,name,type,x,y,width,height}]}`. */
27
+ get_selection(api: PluginApi, _args: unknown): Promise<ToolResponse>;
28
+ /** `figma.get_viewport()` → `{ok, center:{x,y}, zoom}`. */
29
+ get_viewport(api: PluginApi, _args: unknown): Promise<ToolResponse>;
30
+ /** `figma.select_node({nodeId})` — sets `figma.currentPage.selection`. */
31
+ select_node(api: PluginApi, args: unknown): Promise<ToolResponse>;
32
+ /** `figma.move_node({nodeId, dx, dy})` — mutates `node.x`/`node.y` in place. */
33
+ move_node(api: PluginApi, args: unknown): Promise<ToolResponse>;
34
+ /** `figma.create_rectangle({x,y,width,height, fillColor?})` → `{ok, nodeId}`. */
35
+ create_rectangle(api: PluginApi, args: unknown): Promise<ToolResponse>;
36
+ };
37
+ export declare function register(api: PluginApi): void;
38
+ export default register;
package/dist/index.js ADDED
@@ -0,0 +1,220 @@
1
+ // @browxai/plugin-figma — Figma canvas-app adapter.
2
+ //
3
+ // Surfaces a small, useful first-party tool surface (selection, viewport,
4
+ // node mutate, rectangle create) over the page-side `figma.*` global that
5
+ // Figma exposes in its plugin-iframe context (and, for agent-driven
6
+ // sessions, reachable through `eval_js` when the file is open in the
7
+ // editor with the plugin context loaded).
8
+ //
9
+ // Design contract:
10
+ // - All five tools route through `api.callTool("eval_js", {expr})`.
11
+ // - The plugin declares the `eval` + `canvas` capabilities at the
12
+ // manifest level — the host gates the whole plugin against those.
13
+ // - Handlers are resilient to the app not being loaded: a guard
14
+ // `typeof figma === "undefined"` returns the structured
15
+ // `figma-not-loaded` error.
16
+ // - The canonical canvas-app adapter pattern — keep `register(api)`
17
+ // small, push the heavy lifting into the eval-expression strings,
18
+ // parse the result back in the plugin.
19
+ //
20
+ // Targeted API surface (as of 2026-06): figma.viewport.{center,zoom},
21
+ // figma.currentPage.selection, figma.createRectangle(),
22
+ // figma.getNodeById(). These are the stable parts of Figma's plugin API
23
+ // and have been present for years; later versions may add fields but
24
+ // shouldn't break this set.
25
+ const json = (obj) => ({
26
+ content: [{ type: "text", text: JSON.stringify(obj, null, 2) }],
27
+ });
28
+ const NOT_LOADED = {
29
+ ok: false,
30
+ error: "Figma not loaded — open the app first OR the surface is not exposed on this version of the app",
31
+ code: "figma-not-loaded",
32
+ };
33
+ const badArg = (which) => ({
34
+ ok: false,
35
+ error: `bad-arg: missing or invalid \`${which}\``,
36
+ code: "bad-arg",
37
+ });
38
+ /** Parse the first text item of an eval_js MCP envelope as JSON. */
39
+ function parseEvalEnvelope(res) {
40
+ const first = res.content[0];
41
+ if (!first || first.type !== "text") {
42
+ return { ok: false, error: "eval_js returned no text content" };
43
+ }
44
+ try {
45
+ return JSON.parse(first.text);
46
+ }
47
+ catch (e) {
48
+ return { ok: false, error: `eval_js envelope parse failure: ${e.message}` };
49
+ }
50
+ }
51
+ /**
52
+ * Run an eval_js expression and unwrap its envelope to the page-side value.
53
+ *
54
+ * Returns `{ok:true, value}` on a successful page-side eval, or a structured
55
+ * error envelope (passed straight through to the caller) otherwise.
56
+ */
57
+ async function runEval(api, expr) {
58
+ const res = await api.callTool("eval_js", { expr });
59
+ const env = parseEvalEnvelope(res);
60
+ if (!env.ok)
61
+ return { ok: false, error: env.error ?? "eval_js failed" };
62
+ return { ok: true, value: env.value };
63
+ }
64
+ /**
65
+ * Probe `typeof figma` in the page; if undefined, the editor isn't
66
+ * loaded (or the surface isn't exposed for this build) and the caller
67
+ * should return the canonical `figma-not-loaded` envelope.
68
+ */
69
+ async function figmaLoaded(api) {
70
+ const r = await runEval(api, `(typeof figma !== "undefined")`);
71
+ return r.ok && r.value === true;
72
+ }
73
+ export const handlers = {
74
+ /** `figma.get_selection()` → `{ok, nodes:[{id,name,type,x,y,width,height}]}`. */
75
+ async get_selection(api, _args) {
76
+ if (!(await figmaLoaded(api)))
77
+ return json(NOT_LOADED);
78
+ const expr = `(() => {
79
+ const sel = figma.currentPage.selection || [];
80
+ return {
81
+ nodes: sel.map(n => ({
82
+ id: n.id,
83
+ name: n.name,
84
+ type: n.type,
85
+ x: n.x,
86
+ y: n.y,
87
+ width: n.width,
88
+ height: n.height,
89
+ })),
90
+ };
91
+ })()`;
92
+ const r = await runEval(api, expr);
93
+ if (!r.ok)
94
+ return json({ ok: false, error: r.error, code: "eval-failed" });
95
+ return json({ ok: true, ...r.value });
96
+ },
97
+ /** `figma.get_viewport()` → `{ok, center:{x,y}, zoom}`. */
98
+ async get_viewport(api, _args) {
99
+ if (!(await figmaLoaded(api)))
100
+ return json(NOT_LOADED);
101
+ const expr = `(() => {
102
+ const v = figma.viewport;
103
+ return { center: { x: v.center.x, y: v.center.y }, zoom: v.zoom };
104
+ })()`;
105
+ const r = await runEval(api, expr);
106
+ if (!r.ok)
107
+ return json({ ok: false, error: r.error, code: "eval-failed" });
108
+ return json({ ok: true, ...r.value });
109
+ },
110
+ /** `figma.select_node({nodeId})` — sets `figma.currentPage.selection`. */
111
+ async select_node(api, args) {
112
+ const a = (args ?? {});
113
+ if (typeof a.nodeId !== "string" || a.nodeId.length === 0)
114
+ return json(badArg("nodeId"));
115
+ if (!(await figmaLoaded(api)))
116
+ return json(NOT_LOADED);
117
+ const id = JSON.stringify(a.nodeId);
118
+ const expr = `(() => {
119
+ const n = figma.getNodeById(${id});
120
+ if (!n) return { found: false };
121
+ figma.currentPage.selection = [n];
122
+ return { found: true, nodeId: n.id };
123
+ })()`;
124
+ const r = await runEval(api, expr);
125
+ if (!r.ok)
126
+ return json({ ok: false, error: r.error, code: "eval-failed" });
127
+ const v = r.value;
128
+ if (!v.found)
129
+ return json({ ok: false, error: `node not found: ${a.nodeId}`, code: "node-not-found" });
130
+ return json({ ok: true, nodeId: v.nodeId });
131
+ },
132
+ /** `figma.move_node({nodeId, dx, dy})` — mutates `node.x`/`node.y` in place. */
133
+ async move_node(api, args) {
134
+ const a = (args ?? {});
135
+ if (typeof a.nodeId !== "string" || a.nodeId.length === 0)
136
+ return json(badArg("nodeId"));
137
+ if (typeof a.dx !== "number")
138
+ return json(badArg("dx"));
139
+ if (typeof a.dy !== "number")
140
+ return json(badArg("dy"));
141
+ if (!(await figmaLoaded(api)))
142
+ return json(NOT_LOADED);
143
+ const id = JSON.stringify(a.nodeId);
144
+ const expr = `(() => {
145
+ const n = figma.getNodeById(${id});
146
+ if (!n) return { found: false };
147
+ n.x = n.x + (${a.dx});
148
+ n.y = n.y + (${a.dy});
149
+ return { found: true, nodeId: n.id, x: n.x, y: n.y };
150
+ })()`;
151
+ const r = await runEval(api, expr);
152
+ if (!r.ok)
153
+ return json({ ok: false, error: r.error, code: "eval-failed" });
154
+ const v = r.value;
155
+ if (!v.found)
156
+ return json({ ok: false, error: `node not found: ${a.nodeId}`, code: "node-not-found" });
157
+ return json({ ok: true, nodeId: v.nodeId, x: v.x, y: v.y });
158
+ },
159
+ /** `figma.create_rectangle({x,y,width,height, fillColor?})` → `{ok, nodeId}`. */
160
+ async create_rectangle(api, args) {
161
+ const a = (args ?? {});
162
+ if (typeof a.x !== "number")
163
+ return json(badArg("x"));
164
+ if (typeof a.y !== "number")
165
+ return json(badArg("y"));
166
+ if (typeof a.width !== "number" || a.width <= 0)
167
+ return json(badArg("width"));
168
+ if (typeof a.height !== "number" || a.height <= 0)
169
+ return json(badArg("height"));
170
+ const fill = a.fillColor && typeof a.fillColor === "object"
171
+ ? a.fillColor
172
+ : undefined;
173
+ if (fill &&
174
+ (typeof fill.r !== "number" || typeof fill.g !== "number" || typeof fill.b !== "number")) {
175
+ return json(badArg("fillColor"));
176
+ }
177
+ if (!(await figmaLoaded(api)))
178
+ return json(NOT_LOADED);
179
+ const fillExpr = fill
180
+ ? `rect.fills = [{ type: "SOLID", color: { r: ${fill.r}, g: ${fill.g}, b: ${fill.b} } }];`
181
+ : "";
182
+ const expr = `(() => {
183
+ const rect = figma.createRectangle();
184
+ rect.x = ${a.x};
185
+ rect.y = ${a.y};
186
+ rect.resize(${a.width}, ${a.height});
187
+ ${fillExpr}
188
+ return { nodeId: rect.id };
189
+ })()`;
190
+ const r = await runEval(api, expr);
191
+ if (!r.ok)
192
+ return json({ ok: false, error: r.error, code: "eval-failed" });
193
+ const v = r.value;
194
+ return json({ ok: true, nodeId: v.nodeId });
195
+ },
196
+ };
197
+ export function register(api) {
198
+ api.log.info("figma plugin: registering tools", { namespace: api.namespace });
199
+ api.registerTool(`${api.namespace}.get_selection`, {
200
+ description: "Read the current `figma.currentPage.selection` — returns `{ok, nodes:[{id,name,type,x,y,width,height}]}`. App-not-loaded surfaces `code:'figma-not-loaded'`.",
201
+ inputSchema: {},
202
+ }, (args) => handlers.get_selection(api, args));
203
+ api.registerTool(`${api.namespace}.get_viewport`, {
204
+ description: "Read `figma.viewport.center` + `figma.viewport.zoom` — returns `{ok, center:{x,y}, zoom}`. App-not-loaded surfaces `code:'figma-not-loaded'`.",
205
+ inputSchema: {},
206
+ }, (args) => handlers.get_viewport(api, args));
207
+ api.registerTool(`${api.namespace}.select_node`, {
208
+ description: "Set `figma.currentPage.selection` to the node addressed by `nodeId`. Returns `{ok, nodeId}` on success, `{ok:false, code:'node-not-found'|'bad-arg'|'figma-not-loaded'}` otherwise.",
209
+ inputSchema: {},
210
+ }, (args) => handlers.select_node(api, args));
211
+ api.registerTool(`${api.namespace}.move_node`, {
212
+ description: "Translate a node by `(dx, dy)` — mutates `node.x` and `node.y` in place. Returns `{ok, nodeId, x, y}` with the post-move position. App-not-loaded / missing-arg / unknown-id surface structured errors.",
213
+ inputSchema: {},
214
+ }, (args) => handlers.move_node(api, args));
215
+ api.registerTool(`${api.namespace}.create_rectangle`, {
216
+ description: "Create a rectangle via `figma.createRectangle()` at `(x, y)` with `(width, height)`. Optional `fillColor:{r,g,b}` (0–1 floats — Figma's color convention) sets a solid fill. Returns `{ok, nodeId}`.",
217
+ inputSchema: {},
218
+ }, (args) => handlers.create_rectangle(api, args));
219
+ }
220
+ export default register;
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "@browxai/plugin-figma",
3
+ "version": "0.1.0",
4
+ "description": "Figma canvas-app adapter plugin for browxai — surfaces a small first-party tool surface (selection, viewport, node mutate, rectangle create) over the `figma.*` page-side global.",
5
+ "author": "Kalebtec",
6
+ "license": "MIT",
7
+ "type": "module",
8
+ "keywords": [
9
+ "browxai",
10
+ "browxai-plugin",
11
+ "figma",
12
+ "canvas",
13
+ "mcp",
14
+ "browser-automation",
15
+ "ai-agent"
16
+ ],
17
+ "homepage": "https://browxai.com/plugins/first-party/",
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/kalebteccom/browxai.git",
21
+ "directory": "packages/plugins/figma"
22
+ },
23
+ "bugs": {
24
+ "url": "https://github.com/kalebteccom/browxai/issues"
25
+ },
26
+ "main": "dist/index.js",
27
+ "types": "dist/index.d.ts",
28
+ "files": [
29
+ "dist",
30
+ "schema.d.ts",
31
+ "LICENSE"
32
+ ],
33
+ "publishConfig": {
34
+ "access": "public",
35
+ "provenance": true
36
+ },
37
+ "scripts": {
38
+ "build": "tsc -p tsconfig.json",
39
+ "typecheck": "tsc -p tsconfig.json --noEmit",
40
+ "test": "vitest run"
41
+ },
42
+ "browxai": {
43
+ "apiVersion": "1.0.0",
44
+ "browxaiVersion": "^0.7.0",
45
+ "namespace": "figma",
46
+ "register": "dist/index.js",
47
+ "capabilities": [
48
+ "eval",
49
+ "canvas"
50
+ ],
51
+ "trust": "kalebtec",
52
+ "dependsOn": []
53
+ },
54
+ "engines": {
55
+ "node": ">=20"
56
+ },
57
+ "devDependencies": {
58
+ "typescript": "^5.5.0",
59
+ "vitest": "^2.0.0"
60
+ }
61
+ }
package/schema.d.ts ADDED
@@ -0,0 +1,48 @@
1
+ // Typed SDK overlay for `@browxai/plugin-figma` consumers.
2
+ //
3
+ // Compose this schema into the host's `BrowxaiClientWithPlugins` helper to
4
+ // get autocomplete on every figma.* tool:
5
+ //
6
+ // import type { FigmaPluginSchema } from "@browxai/plugin-figma/schema";
7
+ // import type { BrowxaiClientWithPlugins } from "browxai";
8
+ //
9
+ // const client = (await createBrowxai({...})) as BrowxaiClientWithPlugins<FigmaPluginSchema>;
10
+ // await client.plugins.figma.get_selection({});
11
+
12
+ interface FigmaBrowxaiResult {
13
+ readonly content: ReadonlyArray<
14
+ { type: "text"; text: string } | { type: "image"; data: string; mimeType: string }
15
+ >;
16
+ readonly data?: Record<string, unknown>;
17
+ }
18
+
19
+ export interface FigmaSceneNode {
20
+ readonly id: string;
21
+ readonly name: string;
22
+ readonly type: string;
23
+ readonly x: number;
24
+ readonly y: number;
25
+ readonly width: number;
26
+ readonly height: number;
27
+ }
28
+
29
+ export interface FigmaPluginSchema {
30
+ readonly figma: {
31
+ /** Read the current `figma.currentPage.selection` shape. */
32
+ get_selection(args?: Record<string, never>): Promise<FigmaBrowxaiResult>;
33
+ /** Read `figma.viewport.center` + `figma.viewport.zoom`. */
34
+ get_viewport(args?: Record<string, never>): Promise<FigmaBrowxaiResult>;
35
+ /** Set `figma.currentPage.selection` to the node with `nodeId`. */
36
+ select_node(args: { nodeId: string }): Promise<FigmaBrowxaiResult>;
37
+ /** Mutate `node.x += dx; node.y += dy` on the addressed node. */
38
+ move_node(args: { nodeId: string; dx: number; dy: number }): Promise<FigmaBrowxaiResult>;
39
+ /** Create a rectangle via `figma.createRectangle()` — returns `{nodeId}`. */
40
+ create_rectangle(args: {
41
+ x: number;
42
+ y: number;
43
+ width: number;
44
+ height: number;
45
+ fillColor?: { r: number; g: number; b: number };
46
+ }): Promise<FigmaBrowxaiResult>;
47
+ };
48
+ }