@sxl-studio/bridge 1.2.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/README.md +153 -0
- package/dist/bridge-version.d.ts +4 -0
- package/dist/bridge-version.js +18 -0
- package/dist/bridge-version.js.map +1 -0
- package/dist/command-http.d.ts +13 -0
- package/dist/command-http.js +29 -0
- package/dist/command-http.js.map +1 -0
- package/dist/command-queue.d.ts +29 -0
- package/dist/command-queue.js +168 -0
- package/dist/command-queue.js.map +1 -0
- package/dist/http-api.d.ts +8 -0
- package/dist/http-api.js +146 -0
- package/dist/http-api.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +35 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp-factory.d.ts +7 -0
- package/dist/mcp-factory.js +30 -0
- package/dist/mcp-factory.js.map +1 -0
- package/dist/mcp-server.d.ts +9 -0
- package/dist/mcp-server.js +16 -0
- package/dist/mcp-server.js.map +1 -0
- package/dist/session.d.ts +22 -0
- package/dist/session.js +64 -0
- package/dist/session.js.map +1 -0
- package/dist/sxl-mcp-instructions.d.ts +5 -0
- package/dist/sxl-mcp-instructions.js +36 -0
- package/dist/sxl-mcp-instructions.js.map +1 -0
- package/dist/tools/composition.d.ts +6 -0
- package/dist/tools/composition.js +47 -0
- package/dist/tools/composition.js.map +1 -0
- package/dist/tools/data.d.ts +6 -0
- package/dist/tools/data.js +56 -0
- package/dist/tools/data.js.map +1 -0
- package/dist/tools/diagnostics.d.ts +6 -0
- package/dist/tools/diagnostics.js +70 -0
- package/dist/tools/diagnostics.js.map +1 -0
- package/dist/tools/figma-nodes.d.ts +9 -0
- package/dist/tools/figma-nodes.js +347 -0
- package/dist/tools/figma-nodes.js.map +1 -0
- package/dist/tools/figma-rc-extended.d.ts +6 -0
- package/dist/tools/figma-rc-extended.js +54 -0
- package/dist/tools/figma-rc-extended.js.map +1 -0
- package/dist/tools/git.d.ts +6 -0
- package/dist/tools/git.js +40 -0
- package/dist/tools/git.js.map +1 -0
- package/dist/tools/tokens.d.ts +6 -0
- package/dist/tools/tokens.js +66 -0
- package/dist/tools/tokens.js.map +1 -0
- package/dist/types.d.ts +44 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/dist/ws-server.d.ts +31 -0
- package/dist/ws-server.js +120 -0
- package/dist/ws-server.js.map +1 -0
- package/package.json +36 -0
package/README.md
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# SXL Studio Bridge
|
|
2
|
+
|
|
3
|
+
Node.js process that connects **Cursor (or any MCP client)** and **HTTP clients** to the **SXL Studio Figma plugin** while a Remote Connect session is active in the plugin UI.
|
|
4
|
+
|
|
5
|
+
## What it does
|
|
6
|
+
|
|
7
|
+
1. **WebSocket** — plugin UI iframe connects here and forwards `remote-command` / results.
|
|
8
|
+
2. **MCP (stdio)** — Cursor Agent calls tools (`export_variables`, `inspect_selection`, …); each tool maps to one plugin command.
|
|
9
|
+
3. **HTTP** — same command queue for scripts and other tools.
|
|
10
|
+
|
|
11
|
+
Official [Figma MCP](https://developers.figma.com/docs/figma-mcp-server/) does not run your plugin code. Use **both**: Figma MCP for file/design context; **this bridge** for SXL-specific operations (compositions, token export, mappings, etc.).
|
|
12
|
+
|
|
13
|
+
### Figma MCP vs SXL Bridge (product split)
|
|
14
|
+
|
|
15
|
+
Figma documents the hosted and desktop MCP in the Help Center: [Guide to the Figma MCP server](https://help.figma.com/hc/en-us/articles/32132100833559-Guide-to-the-Figma-MCP-server). **Remote** MCP is the default in their guidance; some client features (for example write-to-canvas flows in their compatibility matrix) are oriented toward **remote** MCP rather than a local-only server.
|
|
16
|
+
|
|
17
|
+
| Need | Use |
|
|
18
|
+
|------|-----|
|
|
19
|
+
| Native file operations where Figma’s MCP tools are enough (context, many canvas writes, variables in supported flows) | **Official Figma MCP** (often remote `https://mcp.figma.com/mcp` per Figma docs) |
|
|
20
|
+
| Plugin-only workflows: compositions, plugin token/Git APIs, whitelisted node helpers that run in **this** plugin | **SXL Studio Bridge** + plugin Remote Connect |
|
|
21
|
+
|
|
22
|
+
Bridge forwards `commandType` + `payload` over WebSocket; **execution is always in the plugin** (whitelist + handlers). Adding an MCP tool in Bridge without the matching plugin handler will not work.
|
|
23
|
+
|
|
24
|
+
### Version compatibility (npm Bridge ⟷ plugin)
|
|
25
|
+
|
|
26
|
+
When you add or change remote commands:
|
|
27
|
+
|
|
28
|
+
1. Implement the command in the **plugin** (`WHITELISTED_COMMANDS`, `remote/canvas/` or `remote/sxl/`, etc.).
|
|
29
|
+
2. Register the matching MCP tool (and payload shape) in **Bridge** `src/tools/`.
|
|
30
|
+
3. Bump **Bridge** semver (`package.json`) and document the minimum **plugin** build users need.
|
|
31
|
+
|
|
32
|
+
| `@sxl-studio/bridge` (npm) | SXL Studio plugin |
|
|
33
|
+
|---------------------------|-------------------|
|
|
34
|
+
| `1.0.x` | Same-era plugin with base remote node tools (`create_frame`, `set_node_fill`, …) |
|
|
35
|
+
| `1.1.x` | Plugin build that includes `set_node_fill_variable` and extended `create_frame` / `create_text` (`parentId`, Auto Layout–related optional fields) |
|
|
36
|
+
| `1.2.x` | Plugin build that includes Remote module split (`dispatch/`, `canvas/`, `sxl/`), MCP `instructions`, base canvas tools (`create_rectangle`, `set_auto_layout`, `duplicate_subtree`, `create_component_instance`, `apply_documentation_payload`), plus **extended** Remote Connect canvas tools registered from `src/tools/figma-rc-extended.ts`. Bridge **1.2.0+** reads `package.json` for startup version, aligns **timeouts** with plugin `COMMAND_TIMEOUTS`, and validates **`POST /api/command`** bodies (`commandType` + object `payload`). |
|
|
37
|
+
|
|
38
|
+
The plugin’s own `version` in `Plugin/package.json` may differ from marketing labels; treat **feature alignment** by changelog or release tag, not npm name alone.
|
|
39
|
+
|
|
40
|
+
### Remote node API strategy
|
|
41
|
+
|
|
42
|
+
New canvas automation is shipped as **small atomic** remote commands (variable-bound fills, optional `parentId`, Auto Layout fields) so Bridge and HTTP clients stay in sync with the whitelist. A single high-level command such as `generate_palette_documentation` remains a possible future optimization if you want fewer round-trips for one specific workflow.
|
|
43
|
+
|
|
44
|
+
## Ports
|
|
45
|
+
|
|
46
|
+
**One TCP port** (`BRIDGE_PORT`, default **`37830`** — avoids many apps that bind `3100` on macOS): plugin **WebSocket**, **HTTP REST** (`/api/*`), and **MCP Streamable HTTP** (`/mcp`) share it. Override with `BRIDGE_PORT=3100 npm start` if you need the legacy port (update plugin build + `mcp.json` to match).
|
|
47
|
+
|
|
48
|
+
| On port `BRIDGE_PORT` | URL (default) |
|
|
49
|
+
|----------------------|---------------|
|
|
50
|
+
| Plugin UI | `ws://127.0.0.1:37830` |
|
|
51
|
+
| REST | `http://127.0.0.1:37830/api/status`, `POST …/api/command`, … |
|
|
52
|
+
| Cursor MCP | `http://127.0.0.1:37830/mcp` |
|
|
53
|
+
|
|
54
|
+
### Cursor MCP (recommended)
|
|
55
|
+
|
|
56
|
+
Same pattern as [Figma’s MCP URL](https://developers.figma.com/docs/figma-mcp-server/local-server-installation/): add **`http://127.0.0.1:37830/mcp`** (or `http://127.0.0.1:{BRIDGE_PORT}/mcp`). **Start Bridge first** (`cd Bridge && npm run build && npm start`, or from repo root `npm run bridge` after `npm run bridge:build`). Then enable Remote Connect in the SXL Studio plugin.
|
|
57
|
+
|
|
58
|
+
Official **Figma MCP** (`http://127.0.0.1:3845/mcp`) reads design context from the file; it does **not** run SXL Studio plugin commands. For `create_frame`, `export_variables`, compositions, etc., use **this** Bridge MCP (or `POST /api/command`).
|
|
59
|
+
|
|
60
|
+
## Run locally
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
cd Bridge
|
|
64
|
+
npm install
|
|
65
|
+
npm run build
|
|
66
|
+
node dist/index.js
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
If you see **`EADDRINUSE`**, pick a free port: `BRIDGE_PORT=39999 npm start`, then use **`http://127.0.0.1:39999/mcp`** in Cursor and rebuild the plugin so `remoteBridge.ts` default matches (or keep defaults in sync via env in your workflow). Check listeners: `lsof -nP -iTCP:37830 -sTCP:LISTEN`.
|
|
70
|
+
|
|
71
|
+
The repo **`.cursor/mcp.json`** points Cursor at **`http://127.0.0.1:37830/mcp`**. You must **run Bridge** before opening Agent chat. Optional: stdio spawn via `node scripts/run-sxl-bridge.mjs`. See `Plugin/docs/ru/60-remote-connect.md`.
|
|
72
|
+
|
|
73
|
+
**MCP + stdout:** the Model Context Protocol uses **stdout** for JSON-RPC. Do not use `console.log` in this process (it corrupts the stream). Use **`console.error`** for diagnostics.
|
|
74
|
+
|
|
75
|
+
## MCP configuration (Cursor)
|
|
76
|
+
|
|
77
|
+
Example `.cursor/mcp.json`:
|
|
78
|
+
|
|
79
|
+
```json
|
|
80
|
+
{
|
|
81
|
+
"mcpServers": {
|
|
82
|
+
"sxl-studio": {
|
|
83
|
+
"command": "node",
|
|
84
|
+
"args": ["/absolute/path/to/repo/Bridge/dist/index.js"],
|
|
85
|
+
"env": {
|
|
86
|
+
"BRIDGE_PORT": "37830"
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## HTTP API examples
|
|
94
|
+
|
|
95
|
+
Requires the plugin open in Figma, Remote Connect enabled, and bridge running.
|
|
96
|
+
|
|
97
|
+
**Status** (JSON includes `mcpStreamableHttp` and `ports`; WebSocket and HTTP use the **same** port number)
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
curl -s http://127.0.0.1:37830/api/status
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
**Read token file (after you know `fileId` from MCP `list_token_files`)**
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
curl -s -X POST http://127.0.0.1:37830/api/command \
|
|
107
|
+
-H "Content-Type: application/json" \
|
|
108
|
+
-d '{"commandType":"get_token_file_content","payload":{"fileId":"tf_..."}}'
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
**Save token file**
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
curl -s -X POST http://127.0.0.1:37830/api/command \
|
|
115
|
+
-H "Content-Type: application/json" \
|
|
116
|
+
-d '{"commandType":"save_token_file","payload":{"fileId":"tf_...","content":"{}"}}'
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
**Export composition JSON from current selection**
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
curl -s -X POST http://127.0.0.1:37830/api/command \
|
|
123
|
+
-H "Content-Type: application/json" \
|
|
124
|
+
-d '{"commandType":"export_composition_json","payload":{}}'
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Optional `payload.nodeId` selects a specific node instead of the first selected node.
|
|
128
|
+
|
|
129
|
+
## MCP tools (summary)
|
|
130
|
+
|
|
131
|
+
Registered in `src/mcp-server.ts` via `tools/*.ts`, including:
|
|
132
|
+
|
|
133
|
+
- **Tokens:** `export_variables`, `reset_diff*`, `reapply_token_bindings`, `cross_file_sync_*`, `get_token_file_content`, `save_token_file`
|
|
134
|
+
- **Composition:** `generate_composition`, `apply_composition`, `preview_composition`, `check_composition_linked`, `inspect_selection`, `export_composition_json`
|
|
135
|
+
- **Data / nodes:** `apply_mapping`, `apply_all_mappings`, `count_apply_targets`, `generate_instances`, `apply_image`, direct node helpers in `data.ts` / `figma-nodes.ts` (including `set_node_fill_variable`, `create_rectangle`, `set_auto_layout`, `duplicate_subtree`, `create_component_instance`, `apply_documentation_payload`, extended `create_frame` / `create_text`)
|
|
136
|
+
- **Git:** `git_pull`, `git_hard_pull`, `git_push`
|
|
137
|
+
- **Diagnostics:** plugin status, selection, drift, Code Connect, etc.
|
|
138
|
+
|
|
139
|
+
## Development
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
npm run test # Vitest
|
|
143
|
+
npm run build # tsc → dist/
|
|
144
|
+
npm run lint # tsc --noEmit
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## Publishing to npm
|
|
148
|
+
|
|
149
|
+
See [docs/npm-publish.md](docs/npm-publish.md). The package ships only `dist/` and `README.md` (`files` in `package.json`). Example and monorepo-only scripts live under [examples/](examples/).
|
|
150
|
+
|
|
151
|
+
## Examples (monorepo only)
|
|
152
|
+
|
|
153
|
+
See [examples/README.md](examples/README.md) for how to write consumer scripts against `POST /api/command` without coupling the npm package to your design-system paths.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single source of truth: Bridge/package.json (survives `npm version`).
|
|
3
|
+
*/
|
|
4
|
+
import { readFileSync } from "node:fs";
|
|
5
|
+
import { dirname, join } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
let cached = null;
|
|
8
|
+
export function getBridgePackageVersion() {
|
|
9
|
+
if (cached !== null)
|
|
10
|
+
return cached;
|
|
11
|
+
const dir = dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
const pkgPath = join(dir, "..", "package.json");
|
|
13
|
+
const raw = readFileSync(pkgPath, "utf8");
|
|
14
|
+
const v = JSON.parse(raw).version;
|
|
15
|
+
cached = typeof v === "string" && v.length > 0 ? v : "0.0.0";
|
|
16
|
+
return cached;
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=bridge-version.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bridge-version.js","sourceRoot":"","sources":["../src/bridge-version.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,IAAI,MAAM,GAAkB,IAAI,CAAC;AAEjC,MAAM,UAAU,uBAAuB;IACrC,IAAI,MAAM,KAAK,IAAI;QAAE,OAAO,MAAM,CAAC;IACnC,MAAM,GAAG,GAAG,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;IACpD,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,EAAE,IAAI,EAAE,cAAc,CAAC,CAAC;IAChD,MAAM,GAAG,GAAG,YAAY,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;IAC1C,MAAM,CAAC,GAAI,IAAI,CAAC,KAAK,CAAC,GAAG,CAA0B,CAAC,OAAO,CAAC;IAC5D,MAAM,GAAG,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC;IAC7D,OAAO,MAAM,CAAC;AAChB,CAAC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared parsing/validation for POST /api/command (REST fallback for MCP).
|
|
3
|
+
*/
|
|
4
|
+
export type ParsedHttpCommand = {
|
|
5
|
+
ok: true;
|
|
6
|
+
commandType: string;
|
|
7
|
+
payload: Record<string, unknown>;
|
|
8
|
+
} | {
|
|
9
|
+
ok: false;
|
|
10
|
+
status: 400;
|
|
11
|
+
error: string;
|
|
12
|
+
};
|
|
13
|
+
export declare function parseHttpCommandBody(rawBody: string): ParsedHttpCommand;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared parsing/validation for POST /api/command (REST fallback for MCP).
|
|
3
|
+
*/
|
|
4
|
+
export function parseHttpCommandBody(rawBody) {
|
|
5
|
+
let parsed;
|
|
6
|
+
try {
|
|
7
|
+
parsed = JSON.parse(rawBody);
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
return { ok: false, status: 400, error: "Invalid JSON body" };
|
|
11
|
+
}
|
|
12
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
13
|
+
return { ok: false, status: 400, error: "Body must be a JSON object" };
|
|
14
|
+
}
|
|
15
|
+
const obj = parsed;
|
|
16
|
+
const commandType = obj.commandType;
|
|
17
|
+
if (typeof commandType !== "string" || commandType.length === 0) {
|
|
18
|
+
return { ok: false, status: 400, error: "Missing or invalid 'commandType' field" };
|
|
19
|
+
}
|
|
20
|
+
const payload = obj.payload;
|
|
21
|
+
if (payload === undefined) {
|
|
22
|
+
return { ok: true, commandType, payload: {} };
|
|
23
|
+
}
|
|
24
|
+
if (typeof payload !== "object" || payload === null || Array.isArray(payload)) {
|
|
25
|
+
return { ok: false, status: 400, error: "Field 'payload' must be an object when provided" };
|
|
26
|
+
}
|
|
27
|
+
return { ok: true, commandType, payload: payload };
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=command-http.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"command-http.js","sourceRoot":"","sources":["../src/command-http.ts"],"names":[],"mappings":"AAAA;;GAEG;AAMH,MAAM,UAAU,oBAAoB,CAAC,OAAe;IAClD,IAAI,MAAe,CAAC;IACpB,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAY,CAAC;IAC1C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,KAAK,EAAE,mBAAmB,EAAE,CAAC;IAChE,CAAC;IAED,IAAI,MAAM,KAAK,IAAI,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;QAC3E,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,KAAK,EAAE,4BAA4B,EAAE,CAAC;IACzE,CAAC;IAED,MAAM,GAAG,GAAG,MAAiC,CAAC;IAC9C,MAAM,WAAW,GAAG,GAAG,CAAC,WAAW,CAAC;IACpC,IAAI,OAAO,WAAW,KAAK,QAAQ,IAAI,WAAW,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAChE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,KAAK,EAAE,wCAAwC,EAAE,CAAC;IACrF,CAAC;IAED,MAAM,OAAO,GAAG,GAAG,CAAC,OAAO,CAAC;IAC5B,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;QAC1B,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC;IAChD,CAAC;IACD,IAAI,OAAO,OAAO,KAAK,QAAQ,IAAI,OAAO,KAAK,IAAI,IAAI,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;QAC9E,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,KAAK,EAAE,iDAAiD,EAAE,CAAC;IAC9F,CAAC;IAED,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,OAAkC,EAAE,CAAC;AAChF,CAAC"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CommandQueue — manages command lifecycle for the Bridge.
|
|
3
|
+
*
|
|
4
|
+
* Serial execution: one command at a time.
|
|
5
|
+
* Each command has a timeout. Results are resolved via promises.
|
|
6
|
+
*/
|
|
7
|
+
import type { CommandResult, CommandStatus } from "./types.js";
|
|
8
|
+
/** Per-command client timeouts (must stay aligned with plugin `COMMAND_TIMEOUTS` where applicable). */
|
|
9
|
+
export declare const BRIDGE_COMMAND_TIMEOUT_MS: Readonly<Record<string, number>>;
|
|
10
|
+
type CommandSender = (command: {
|
|
11
|
+
commandId: string;
|
|
12
|
+
commandType: string;
|
|
13
|
+
payload: Record<string, unknown>;
|
|
14
|
+
}) => boolean;
|
|
15
|
+
export declare class CommandQueue {
|
|
16
|
+
private queue;
|
|
17
|
+
private current;
|
|
18
|
+
private sendCommand;
|
|
19
|
+
private timeoutHandle;
|
|
20
|
+
setSender(sender: CommandSender): void;
|
|
21
|
+
get queueLength(): number;
|
|
22
|
+
get currentCommand(): string | null;
|
|
23
|
+
execute(commandType: string, payload?: Record<string, unknown>): Promise<CommandResult>;
|
|
24
|
+
resolveCommand(commandId: string, status: CommandStatus, result?: unknown, error?: string, events?: unknown[], durationMs?: number): void;
|
|
25
|
+
cancelAll(): void;
|
|
26
|
+
private processNext;
|
|
27
|
+
private clearTimeout;
|
|
28
|
+
}
|
|
29
|
+
export {};
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CommandQueue — manages command lifecycle for the Bridge.
|
|
3
|
+
*
|
|
4
|
+
* Serial execution: one command at a time.
|
|
5
|
+
* Each command has a timeout. Results are resolved via promises.
|
|
6
|
+
*/
|
|
7
|
+
import { randomUUID } from "node:crypto";
|
|
8
|
+
/** Per-command client timeouts (must stay aligned with plugin `COMMAND_TIMEOUTS` where applicable). */
|
|
9
|
+
export const BRIDGE_COMMAND_TIMEOUT_MS = {
|
|
10
|
+
export_variables: 120_000,
|
|
11
|
+
git_pull: 180_000,
|
|
12
|
+
git_hard_pull: 180_000,
|
|
13
|
+
git_push: 180_000,
|
|
14
|
+
generate_composition: 60_000,
|
|
15
|
+
apply_composition: 30_000,
|
|
16
|
+
apply_all_mappings: 30_000,
|
|
17
|
+
export_composition_json: 60_000,
|
|
18
|
+
get_token_file_content: 30_000,
|
|
19
|
+
save_token_file: 30_000,
|
|
20
|
+
duplicate_subtree: 60_000,
|
|
21
|
+
apply_documentation_payload: 45_000,
|
|
22
|
+
generate_instances: 30_000,
|
|
23
|
+
reapply_token_bindings: 30_000,
|
|
24
|
+
reapply_compositions: 60_000,
|
|
25
|
+
cross_file_sync_fetch: 30_000,
|
|
26
|
+
cross_file_sync_apply: 30_000,
|
|
27
|
+
/** Aligned with Plugin `COMMAND_TIMEOUTS` (remote/types.ts) for canvas-heavy commands */
|
|
28
|
+
export_as_svg: 60_000,
|
|
29
|
+
list_available_fonts: 45_000,
|
|
30
|
+
boolean_operation: 30_000,
|
|
31
|
+
flatten_nodes: 30_000,
|
|
32
|
+
get_local_styles: 30_000,
|
|
33
|
+
list_components: 30_000,
|
|
34
|
+
find_nodes: 30_000,
|
|
35
|
+
get_page_structure: 30_000,
|
|
36
|
+
get_variables: 30_000,
|
|
37
|
+
import_component_by_key: 30_000,
|
|
38
|
+
};
|
|
39
|
+
const DEFAULT_TIMEOUT = 15_000;
|
|
40
|
+
export class CommandQueue {
|
|
41
|
+
queue = [];
|
|
42
|
+
current = null;
|
|
43
|
+
sendCommand = null;
|
|
44
|
+
timeoutHandle = null;
|
|
45
|
+
setSender(sender) {
|
|
46
|
+
this.sendCommand = sender;
|
|
47
|
+
}
|
|
48
|
+
get queueLength() {
|
|
49
|
+
return this.queue.length;
|
|
50
|
+
}
|
|
51
|
+
get currentCommand() {
|
|
52
|
+
return this.current?.commandType ?? null;
|
|
53
|
+
}
|
|
54
|
+
async execute(commandType, payload = {}) {
|
|
55
|
+
const commandId = randomUUID();
|
|
56
|
+
const timeout = BRIDGE_COMMAND_TIMEOUT_MS[commandType] ?? DEFAULT_TIMEOUT;
|
|
57
|
+
return new Promise((resolve) => {
|
|
58
|
+
const command = {
|
|
59
|
+
commandId,
|
|
60
|
+
commandType,
|
|
61
|
+
payload,
|
|
62
|
+
status: "pending",
|
|
63
|
+
createdAt: Date.now(),
|
|
64
|
+
resolve,
|
|
65
|
+
};
|
|
66
|
+
this.queue.push(command);
|
|
67
|
+
this.processNext();
|
|
68
|
+
setTimeout(() => {
|
|
69
|
+
if (command.status === "pending" || command.status === "running") {
|
|
70
|
+
command.status = "timeout";
|
|
71
|
+
command.completedAt = Date.now();
|
|
72
|
+
command.error = `Command "${commandType}" timed out after ${timeout}ms`;
|
|
73
|
+
resolve({
|
|
74
|
+
commandId,
|
|
75
|
+
status: "timeout",
|
|
76
|
+
error: command.error,
|
|
77
|
+
});
|
|
78
|
+
if (this.current?.commandId === commandId) {
|
|
79
|
+
this.current = null;
|
|
80
|
+
this.clearTimeout();
|
|
81
|
+
this.processNext();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}, timeout);
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
resolveCommand(commandId, status, result, error, events, durationMs) {
|
|
88
|
+
const target = this.current?.commandId === commandId
|
|
89
|
+
? this.current
|
|
90
|
+
: this.queue.find((c) => c.commandId === commandId);
|
|
91
|
+
if (!target)
|
|
92
|
+
return;
|
|
93
|
+
target.status = status;
|
|
94
|
+
target.completedAt = Date.now();
|
|
95
|
+
target.result = result;
|
|
96
|
+
target.error = error;
|
|
97
|
+
target.events = events;
|
|
98
|
+
target.durationMs = durationMs;
|
|
99
|
+
target.resolve?.({
|
|
100
|
+
commandId,
|
|
101
|
+
status,
|
|
102
|
+
result,
|
|
103
|
+
error,
|
|
104
|
+
events,
|
|
105
|
+
durationMs,
|
|
106
|
+
});
|
|
107
|
+
if (this.current?.commandId === commandId) {
|
|
108
|
+
this.current = null;
|
|
109
|
+
this.clearTimeout();
|
|
110
|
+
this.processNext();
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
cancelAll() {
|
|
114
|
+
for (const cmd of this.queue) {
|
|
115
|
+
if (cmd.status === "pending") {
|
|
116
|
+
cmd.status = "cancelled";
|
|
117
|
+
cmd.resolve?.({ commandId: cmd.commandId, status: "cancelled", error: "Session disconnected" });
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
this.queue = [];
|
|
121
|
+
if (this.current && this.current.status === "running") {
|
|
122
|
+
this.current.status = "cancelled";
|
|
123
|
+
this.current.resolve?.({ commandId: this.current.commandId, status: "cancelled", error: "Session disconnected" });
|
|
124
|
+
this.current = null;
|
|
125
|
+
}
|
|
126
|
+
this.clearTimeout();
|
|
127
|
+
}
|
|
128
|
+
processNext() {
|
|
129
|
+
if (this.current)
|
|
130
|
+
return;
|
|
131
|
+
if (this.queue.length === 0)
|
|
132
|
+
return;
|
|
133
|
+
const next = this.queue.shift();
|
|
134
|
+
if (!next)
|
|
135
|
+
return;
|
|
136
|
+
if (next.status !== "pending") {
|
|
137
|
+
this.processNext();
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
if (!this.sendCommand) {
|
|
141
|
+
next.status = "failed";
|
|
142
|
+
next.resolve?.({ commandId: next.commandId, status: "failed", error: "No plugin connected" });
|
|
143
|
+
this.processNext();
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
const sent = this.sendCommand({
|
|
147
|
+
commandId: next.commandId,
|
|
148
|
+
commandType: next.commandType,
|
|
149
|
+
payload: next.payload,
|
|
150
|
+
});
|
|
151
|
+
if (!sent) {
|
|
152
|
+
next.status = "failed";
|
|
153
|
+
next.resolve?.({ commandId: next.commandId, status: "failed", error: "Failed to send to plugin" });
|
|
154
|
+
this.processNext();
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
next.status = "running";
|
|
158
|
+
next.startedAt = Date.now();
|
|
159
|
+
this.current = next;
|
|
160
|
+
}
|
|
161
|
+
clearTimeout() {
|
|
162
|
+
if (this.timeoutHandle) {
|
|
163
|
+
clearTimeout(this.timeoutHandle);
|
|
164
|
+
this.timeoutHandle = null;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
//# sourceMappingURL=command-queue.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"command-queue.js","sourceRoot":"","sources":["../src/command-queue.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP REST API for Bridge server.
|
|
3
|
+
*
|
|
4
|
+
* Also serves MCP Streamable HTTP at GET|POST /mcp (recommended for Cursor — same pattern
|
|
5
|
+
* as Figma's official MCP: add server URL in Cursor settings).
|
|
6
|
+
*/
|
|
7
|
+
import type { BridgeWSServer } from "./ws-server.js";
|
|
8
|
+
export declare function startHttpApi(bridgePort: number, wsServer: BridgeWSServer): Promise<void>;
|
package/dist/http-api.js
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP REST API for Bridge server.
|
|
3
|
+
*
|
|
4
|
+
* Also serves MCP Streamable HTTP at GET|POST /mcp (recommended for Cursor — same pattern
|
|
5
|
+
* as Figma's official MCP: add server URL in Cursor settings).
|
|
6
|
+
*/
|
|
7
|
+
import { createServer } from "node:http";
|
|
8
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
9
|
+
import { createBridgeMcpServer } from "./mcp-factory.js";
|
|
10
|
+
import { parseHttpCommandBody } from "./command-http.js";
|
|
11
|
+
function readBody(req) {
|
|
12
|
+
return new Promise((resolve, reject) => {
|
|
13
|
+
let body = "";
|
|
14
|
+
req.on("data", (chunk) => {
|
|
15
|
+
body += chunk.toString();
|
|
16
|
+
});
|
|
17
|
+
req.on("end", () => resolve(body));
|
|
18
|
+
req.on("error", reject);
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
function sendJson(res, status, data) {
|
|
22
|
+
res.writeHead(status, {
|
|
23
|
+
"Content-Type": "application/json",
|
|
24
|
+
"Access-Control-Allow-Origin": "*",
|
|
25
|
+
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
|
26
|
+
"Access-Control-Allow-Headers": "Content-Type, Accept, Authorization, Mcp-Session-Id, mcp-protocol-version",
|
|
27
|
+
});
|
|
28
|
+
res.end(JSON.stringify(data, null, 2));
|
|
29
|
+
}
|
|
30
|
+
function pathnameOnly(reqUrl) {
|
|
31
|
+
try {
|
|
32
|
+
return new URL(reqUrl || "/", "http://127.0.0.1").pathname;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return "/";
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export async function startHttpApi(bridgePort, wsServer) {
|
|
39
|
+
const queue = wsServer.commandQueue;
|
|
40
|
+
const session = wsServer.session;
|
|
41
|
+
const mcpHttpServer = createBridgeMcpServer(queue);
|
|
42
|
+
const mcpTransport = new StreamableHTTPServerTransport({
|
|
43
|
+
sessionIdGenerator: undefined,
|
|
44
|
+
enableDnsRebindingProtection: true,
|
|
45
|
+
});
|
|
46
|
+
await mcpHttpServer.connect(mcpTransport);
|
|
47
|
+
/** WebSocket upgrades, REST `/api/*`, and MCP `/mcp` share this single port. */
|
|
48
|
+
const mcpUrl = `http://127.0.0.1:${bridgePort}/mcp`;
|
|
49
|
+
const server = createServer(async (req, res) => {
|
|
50
|
+
const pathname = pathnameOnly(req.url || "");
|
|
51
|
+
// CORS preflight (REST + MCP)
|
|
52
|
+
if (req.method === "OPTIONS") {
|
|
53
|
+
const reqHdr = req.headers["access-control-request-headers"];
|
|
54
|
+
res.writeHead(204, {
|
|
55
|
+
"Access-Control-Allow-Origin": "*",
|
|
56
|
+
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
|
57
|
+
"Access-Control-Allow-Headers": typeof reqHdr === "string" && reqHdr.length > 0
|
|
58
|
+
? reqHdr
|
|
59
|
+
: "Content-Type, Accept, Authorization, Mcp-Session-Id, mcp-protocol-version",
|
|
60
|
+
});
|
|
61
|
+
res.end();
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
// MCP Streamable HTTP (Cursor, same idea as Figma MCP URL in docs)
|
|
66
|
+
if (pathname === "/mcp") {
|
|
67
|
+
let parsedBody = undefined;
|
|
68
|
+
if (req.method === "POST") {
|
|
69
|
+
const raw = await readBody(req);
|
|
70
|
+
if (raw.length > 0) {
|
|
71
|
+
try {
|
|
72
|
+
parsedBody = JSON.parse(raw);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
parsedBody = raw;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
await mcpTransport.handleRequest(req, res, parsedBody);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
// GET /api/status
|
|
83
|
+
if (req.method === "GET" && pathname === "/api/status") {
|
|
84
|
+
const status = {
|
|
85
|
+
ports: {
|
|
86
|
+
websocket: bridgePort,
|
|
87
|
+
http: bridgePort,
|
|
88
|
+
},
|
|
89
|
+
mcpStreamableHttp: mcpUrl,
|
|
90
|
+
session: session.info,
|
|
91
|
+
queueLength: queue.queueLength,
|
|
92
|
+
currentCommand: queue.currentCommand,
|
|
93
|
+
};
|
|
94
|
+
sendJson(res, 200, status);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
// GET /api/session
|
|
98
|
+
if (req.method === "GET" && pathname === "/api/session") {
|
|
99
|
+
sendJson(res, 200, session.info);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
// POST /api/command
|
|
103
|
+
if (req.method === "POST" && pathname === "/api/command") {
|
|
104
|
+
if (!session.connected) {
|
|
105
|
+
sendJson(res, 503, { error: "No plugin connected" });
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const body = await readBody(req);
|
|
109
|
+
const parsed = parseHttpCommandBody(body);
|
|
110
|
+
if (!parsed.ok) {
|
|
111
|
+
sendJson(res, parsed.status, { error: parsed.error });
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
const result = await queue.execute(parsed.commandType, parsed.payload);
|
|
115
|
+
sendJson(res, 200, result);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
// POST /api/disconnect
|
|
119
|
+
if (req.method === "POST" && pathname === "/api/disconnect") {
|
|
120
|
+
wsServer.disconnectPluginSocket(4003, "Disconnected via HTTP API");
|
|
121
|
+
sendJson(res, 200, { ok: true });
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
sendJson(res, 404, { error: "Not found" });
|
|
125
|
+
}
|
|
126
|
+
catch (err) {
|
|
127
|
+
sendJson(res, 500, { error: String(err) });
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
wsServer.attachToHttpServer(server);
|
|
131
|
+
await new Promise((resolve, reject) => {
|
|
132
|
+
server.on("error", (err) => {
|
|
133
|
+
if (err.code === "EADDRINUSE") {
|
|
134
|
+
console.error(`[Bridge] Port ${bridgePort} already in use (WebSocket + HTTP + MCP share one port).`);
|
|
135
|
+
console.error(`[Bridge] Fix: quit the other process or set BRIDGE_PORT to a free port (e.g. 3110).`);
|
|
136
|
+
console.error(`[Bridge] macOS: lsof -nP -iTCP:${bridgePort} -sTCP:LISTEN`);
|
|
137
|
+
}
|
|
138
|
+
reject(err);
|
|
139
|
+
});
|
|
140
|
+
server.listen(bridgePort, "127.0.0.1", () => {
|
|
141
|
+
console.error(`[Bridge] Listening on http://127.0.0.1:${bridgePort} (REST /api/*, MCP /mcp, plugin ws://127.0.0.1:${bridgePort})`);
|
|
142
|
+
resolve();
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
//# sourceMappingURL=http-api.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http-api.js","sourceRoot":"","sources":["../src/http-api.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* SXL Studio Bridge — Entry Point
|
|
4
|
+
*
|
|
5
|
+
* One TCP port (BRIDGE_PORT): WebSocket upgrades (plugin) + HTTP REST (/api/*) + MCP (/mcp).
|
|
6
|
+
* Plus MCP over stdio for Cursor when configured to spawn the process.
|
|
7
|
+
*/
|
|
8
|
+
import { getBridgePackageVersion } from "./bridge-version.js";
|
|
9
|
+
import { BridgeWSServer } from "./ws-server.js";
|
|
10
|
+
import { startMcpServer } from "./mcp-server.js";
|
|
11
|
+
import { startHttpApi } from "./http-api.js";
|
|
12
|
+
/** Default below avoids common clashes with other dev tools on 3100 (seen on some macOS setups). */
|
|
13
|
+
const PORT = parseInt(process.env["BRIDGE_PORT"] || "37830", 10);
|
|
14
|
+
async function main() {
|
|
15
|
+
console.error("=== SXL Studio Bridge ===");
|
|
16
|
+
console.error(`Version: ${getBridgePackageVersion()}`);
|
|
17
|
+
console.error(`Single port ${PORT}: ws://127.0.0.1:${PORT} + http://127.0.0.1:${PORT}/api/* + http://127.0.0.1:${PORT}/mcp`);
|
|
18
|
+
console.error("");
|
|
19
|
+
const wsServer = new BridgeWSServer();
|
|
20
|
+
await startHttpApi(PORT, wsServer);
|
|
21
|
+
await startMcpServer(wsServer.commandQueue);
|
|
22
|
+
// Graceful shutdown
|
|
23
|
+
const shutdown = () => {
|
|
24
|
+
console.error("\n[Bridge] Shutting down...");
|
|
25
|
+
wsServer.stop();
|
|
26
|
+
process.exit(0);
|
|
27
|
+
};
|
|
28
|
+
process.on("SIGINT", shutdown);
|
|
29
|
+
process.on("SIGTERM", shutdown);
|
|
30
|
+
}
|
|
31
|
+
main().catch((err) => {
|
|
32
|
+
console.error("[Bridge] Fatal error:", err);
|
|
33
|
+
process.exit(1);
|
|
34
|
+
});
|
|
35
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA;;;;;GAKG;AAEH,OAAO,EAAE,uBAAuB,EAAE,MAAM,qBAAqB,CAAC;AAC9D,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACjD,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAE7C,oGAAoG;AACpG,MAAM,IAAI,GAAG,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC,IAAI,OAAO,EAAE,EAAE,CAAC,CAAC;AAEjE,KAAK,UAAU,IAAI;IACjB,OAAO,CAAC,KAAK,CAAC,2BAA2B,CAAC,CAAC;IAC3C,OAAO,CAAC,KAAK,CAAC,YAAY,uBAAuB,EAAE,EAAE,CAAC,CAAC;IACvD,OAAO,CAAC,KAAK,CACX,eAAe,IAAI,oBAAoB,IAAI,yBAAyB,IAAI,+BAA+B,IAAI,MAAM,CAClH,CAAC;IACF,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;IAElB,MAAM,QAAQ,GAAG,IAAI,cAAc,EAAE,CAAC;IAEtC,MAAM,YAAY,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;IAEnC,MAAM,cAAc,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAC;IAE5C,oBAAoB;IACpB,MAAM,QAAQ,GAAG,GAAG,EAAE;QACpB,OAAO,CAAC,KAAK,CAAC,6BAA6B,CAAC,CAAC;QAC7C,QAAQ,CAAC,IAAI,EAAE,CAAC;QAChB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC,CAAC;IAEF,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;IAC/B,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;AAClC,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;IACnB,OAAO,CAAC,KAAK,CAAC,uBAAuB,EAAE,GAAG,CAAC,CAAC;IAC5C,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
|