genable-mcp 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 +21 -0
- package/README.md +156 -0
- package/dist/httpBridge.js +186 -0
- package/dist/index.js +121 -0
- package/dist/tools-schema.json +1220 -0
- package/dist/wsRelay.js +272 -0
- package/package.json +54 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Genable
|
|
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,156 @@
|
|
|
1
|
+
# genable-mcp
|
|
2
|
+
|
|
3
|
+
**The write-side complement to Figma's official MCP.** Build, edit, and search Figma nodes from any MCP client (Claude Code, Cursor, etc.) via the Genable plugin.
|
|
4
|
+
|
|
5
|
+
## What this is
|
|
6
|
+
|
|
7
|
+
Figma's official MCP is excellent for **reading** designs (`get_design_context`, code generation). But it's mostly read-only — there's no first-class way to write to the canvas, navigate across pages, or run plugin-API code from your MCP client.
|
|
8
|
+
|
|
9
|
+
`genable-mcp` fills that gap. It exposes 39 tools focused on the **write** side:
|
|
10
|
+
|
|
11
|
+
- **Tree creation** — build complete subtrees with JSX-like markup (`jsx`)
|
|
12
|
+
- **Property edits** — text, fills, strokes, layout, all auto-layout aware (`set_text`, `set_fill`, `set_layout`, `set_stroke`, `edit`)
|
|
13
|
+
- **Variables / tokens** — collections, modes, bindings (`create_variable`, `bind_variable`, `set_variable_mode`, etc.)
|
|
14
|
+
- **Components** — create, combine, expose props, instance (`create_component`, `add_component_prop`, `create_instance`)
|
|
15
|
+
- **Cross-page navigation** — `switch_page` (officially the painful gap)
|
|
16
|
+
- **Search & inspect** — `find_nodes`, `inspect`, `describe`, `find_references`
|
|
17
|
+
- **Visual verification** — `get_screenshot`
|
|
18
|
+
- **Escape hatch** — `js` for raw plugin API access (with sandbox-limit docs in the tool description)
|
|
19
|
+
|
|
20
|
+
We **recommend pairing** with Figma's official MCP. They cover read-for-codegen; we cover write-and-edit. The two MCPs together give an LLM full read+write access to a Figma file.
|
|
21
|
+
|
|
22
|
+
## How it works
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
MCP client (Claude Code / Cursor / etc.)
|
|
26
|
+
↓ stdio JSON-RPC
|
|
27
|
+
genable-mcp (this package, Node.js)
|
|
28
|
+
↓ WebSocket :3458
|
|
29
|
+
Genable plugin (running inside Figma)
|
|
30
|
+
↓ Figma Plugin API
|
|
31
|
+
Figma file
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
The plugin runs in your Figma desktop app. `genable-mcp` is the bridge that lets external MCP clients call into it.
|
|
35
|
+
|
|
36
|
+
## Setup
|
|
37
|
+
|
|
38
|
+
### 1. Install the Genable plugin in Figma
|
|
39
|
+
|
|
40
|
+
Search "Genable" in the Figma Community and install. Open it once in any file — it auto-connects to localhost:3458.
|
|
41
|
+
|
|
42
|
+
(One-time. Plugin keeps connecting silently after the first run.)
|
|
43
|
+
|
|
44
|
+
### 2. Add `genable-mcp` to your MCP client config
|
|
45
|
+
|
|
46
|
+
#### Claude Code
|
|
47
|
+
|
|
48
|
+
```json
|
|
49
|
+
// .mcp.json (project) or ~/.claude.json (global)
|
|
50
|
+
{
|
|
51
|
+
"mcpServers": {
|
|
52
|
+
"genable": {
|
|
53
|
+
"command": "npx",
|
|
54
|
+
"args": ["-y", "genable-mcp"]
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
#### Cursor / Cline / other MCP clients
|
|
61
|
+
|
|
62
|
+
Same idea — configure a STDIO server with `command: npx`, `args: ["-y", "genable-mcp"]`.
|
|
63
|
+
|
|
64
|
+
### 3. Verify
|
|
65
|
+
|
|
66
|
+
In your MCP client, ask: *"List the pages in my Figma file."* If the plugin is running, you'll see the page roster.
|
|
67
|
+
|
|
68
|
+
## Pair with the official Figma MCP (recommended)
|
|
69
|
+
|
|
70
|
+
```json
|
|
71
|
+
{
|
|
72
|
+
"mcpServers": {
|
|
73
|
+
"figma": { /* official, read */ },
|
|
74
|
+
"genable": {
|
|
75
|
+
"command": "npx",
|
|
76
|
+
"args": ["-y", "genable-mcp"]
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Rule of thumb:
|
|
83
|
+
- **Figma official** → "read this design and give me code" workflows
|
|
84
|
+
- **Genable** → "build / edit / restructure this design" workflows
|
|
85
|
+
- Both together → end-to-end "code ↔ Figma" round-trips
|
|
86
|
+
|
|
87
|
+
## Configuration
|
|
88
|
+
|
|
89
|
+
| Env var | Default | Purpose |
|
|
90
|
+
|---|---|---|
|
|
91
|
+
| `MCP_WS_PORT` | `3458` | Port the WebSocket relay listens on |
|
|
92
|
+
| `RELAY_SECRET` | (empty) | If set, plugin must send matching secret in `identify` handshake. Use when sharing a host between multiple users. |
|
|
93
|
+
|
|
94
|
+
## Tool reference
|
|
95
|
+
|
|
96
|
+
Each tool's full description (parameters, examples, sandbox limits) is exposed via `ListTools` in the MCP protocol — your client surfaces them automatically. Below is a one-line index.
|
|
97
|
+
|
|
98
|
+
### Tree creation
|
|
99
|
+
- `jsx` — Build a complete subtree with JSX-like markup. Single-call atomicity.
|
|
100
|
+
|
|
101
|
+
### Read
|
|
102
|
+
- `inspect` — Read a node with selectable property facets (layout, paint, typography, etc.).
|
|
103
|
+
- `describe` — Lint-style summary of a subtree.
|
|
104
|
+
- `find_nodes` — Search by name/type within current page.
|
|
105
|
+
- `discover_props` — Unique property values across a subtree.
|
|
106
|
+
- `find_references` — Reverse lookup: who binds this variable?
|
|
107
|
+
- `get_selection` — User's current Figma selection.
|
|
108
|
+
|
|
109
|
+
### Write — properties
|
|
110
|
+
- `edit` — Generic property updates on existing nodes.
|
|
111
|
+
- `set_text`, `set_fill`, `set_stroke`, `set_layout` — Single-intent setters (font load + fallback included).
|
|
112
|
+
- `replace_props` — Bulk find/replace of property values across a subtree.
|
|
113
|
+
|
|
114
|
+
### Write — structure
|
|
115
|
+
- `delete_node`, `move_node`, `clone_node` — Tree mutations.
|
|
116
|
+
|
|
117
|
+
### Components
|
|
118
|
+
- `create_component`, `combine_components` — Promote nodes to components.
|
|
119
|
+
- `add_component_prop`, `list_component_props` — Variant / boolean / instance-swap props.
|
|
120
|
+
- `create_instance` — Instantiate a component.
|
|
121
|
+
|
|
122
|
+
### Variables / tokens
|
|
123
|
+
- `list_variables` — Inventory of collections + variables in the file.
|
|
124
|
+
- `create_collection`, `ensure_collection` — Token collections (idempotent ensure).
|
|
125
|
+
- `create_variable`, `ensure_variable`, `set_variable_value`, `set_variable_mode` — Variable lifecycle.
|
|
126
|
+
- `bind_variable` — Bind a variable to a node property.
|
|
127
|
+
|
|
128
|
+
### Knowledge readers
|
|
129
|
+
- `skill`, `style`, `anatomy`, `guideline`, `help` — Curated design knowledge baked into the plugin.
|
|
130
|
+
|
|
131
|
+
### Page navigation
|
|
132
|
+
- `switch_page` — Switch active page by ID or name. Returns the full page roster.
|
|
133
|
+
|
|
134
|
+
### Visual verification
|
|
135
|
+
- `get_screenshot` — Export a node as PNG, embedded as MCP image content.
|
|
136
|
+
|
|
137
|
+
### Interaction
|
|
138
|
+
- `ask_user` — Pause and ask the user a question (interactive client only).
|
|
139
|
+
- `subtask` — Delegate a sub-prompt to a focused agent.
|
|
140
|
+
|
|
141
|
+
### Escape hatch
|
|
142
|
+
- `js` — `[advanced]` Run JS in the plugin runtime. Full `figma.*` API access. Sandbox limits documented in the tool description (cross-page loading, frozen arrays, font requirements, etc.).
|
|
143
|
+
|
|
144
|
+
## Limitations
|
|
145
|
+
|
|
146
|
+
- **Plugin must be open** — Figma writes require the plugin runtime. The plugin reconnects silently across files; you only need to launch it once per Figma session.
|
|
147
|
+
- **One file at a time per port** — Multi-file workflows: spawn additional relay ports via `MCP_WS_PORTS=3458,3459,…`
|
|
148
|
+
- **Sandbox quirks** — Some Figma plugin-API edges are sharp (font loading, frozen `fills` arrays, stale node IDs after reload). High-level tools wrap most of these; the `js` escape hatch description lists the rest.
|
|
149
|
+
|
|
150
|
+
## License
|
|
151
|
+
|
|
152
|
+
MIT.
|
|
153
|
+
|
|
154
|
+
## Repo
|
|
155
|
+
|
|
156
|
+
Source + issues: [github.com/muse40007/figma-ai-generator-dogfood](https://github.com/muse40007/figma-ai-generator-dogfood) (subdir `tools/mcp-server`).
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @file httpBridge.ts
|
|
4
|
+
* @description Standalone HTTP bridge to Figma plugin — no MCP protocol needed.
|
|
5
|
+
* Any AI CLI (Claude Code, OpenCode, Cursor, etc.) can call Figma tools via curl.
|
|
6
|
+
*
|
|
7
|
+
* Architecture:
|
|
8
|
+
* AI Client (curl / fetch)
|
|
9
|
+
* → This HTTP server (localhost:3460)
|
|
10
|
+
* → WebSocket relay (port 3461, same process) → Figma Plugin
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* npx tsx tools/mcp-server/httpBridge.ts
|
|
14
|
+
* # or with custom ports:
|
|
15
|
+
* HTTP_PORT=3462 MCP_WS_PORT=3463 npx tsx tools/mcp-server/httpBridge.ts
|
|
16
|
+
*
|
|
17
|
+
* Endpoints:
|
|
18
|
+
* GET /health — connection status + connected files
|
|
19
|
+
* GET /clients — list connected Figma files (fileKey, fileName)
|
|
20
|
+
* GET /tools — list available tools
|
|
21
|
+
* POST /tool/:name — call a tool (JSON body = parameters)
|
|
22
|
+
* POST /tool/:name?file=KEY — call a tool targeting a specific Figma file
|
|
23
|
+
*/
|
|
24
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
25
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
26
|
+
};
|
|
27
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
28
|
+
const http_1 = __importDefault(require("http"));
|
|
29
|
+
const fs_1 = require("fs");
|
|
30
|
+
const path_1 = require("path");
|
|
31
|
+
const HTTP_PORT = parseInt(process.env.HTTP_PORT || '3460', 10);
|
|
32
|
+
const WS_PORT = parseInt(process.env.MCP_WS_PORT || '3461', 10);
|
|
33
|
+
function loadToolsSchema() {
|
|
34
|
+
const schemaPath = (0, path_1.join)(__dirname, 'tools-schema.json');
|
|
35
|
+
try {
|
|
36
|
+
return JSON.parse((0, fs_1.readFileSync)(schemaPath, 'utf-8'));
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
console.error(`[HTTP Bridge] FATAL: failed to load ${schemaPath}: ${err.message}`);
|
|
40
|
+
console.error('[HTTP Bridge] Run `npx tsx tools/mcp-server/extract-schema.ts` to regenerate.');
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
// ── Helpers ──
|
|
45
|
+
function jsonResponse(res, status, data) {
|
|
46
|
+
res.writeHead(status, {
|
|
47
|
+
'Content-Type': 'application/json',
|
|
48
|
+
'Access-Control-Allow-Origin': '*',
|
|
49
|
+
});
|
|
50
|
+
res.end(JSON.stringify(data));
|
|
51
|
+
}
|
|
52
|
+
function readBody(req) {
|
|
53
|
+
return new Promise((resolve, reject) => {
|
|
54
|
+
const chunks = [];
|
|
55
|
+
req.on('data', (c) => chunks.push(c));
|
|
56
|
+
req.on('end', () => resolve(Buffer.concat(chunks).toString()));
|
|
57
|
+
req.on('error', reject);
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
// ── Main ──
|
|
61
|
+
async function main() {
|
|
62
|
+
// Set WS port BEFORE importing wsRelay — it reads env at module top-level
|
|
63
|
+
process.env.MCP_WS_PORT = String(WS_PORT);
|
|
64
|
+
const { createWsRelay } = await import('./wsRelay.js');
|
|
65
|
+
const unifiedTools = loadToolsSchema();
|
|
66
|
+
const relay = createWsRelay();
|
|
67
|
+
// Tool catalog for /tools endpoint
|
|
68
|
+
const toolCatalog = unifiedTools.map((def) => ({
|
|
69
|
+
name: def.name,
|
|
70
|
+
description: def.description,
|
|
71
|
+
parameters: def.parameters,
|
|
72
|
+
}));
|
|
73
|
+
const toolSet = new Set(unifiedTools.map((d) => d.name));
|
|
74
|
+
const server = http_1.default.createServer(async (req, res) => {
|
|
75
|
+
const url = new URL(req.url || '/', `http://localhost:${HTTP_PORT}`);
|
|
76
|
+
// CORS preflight
|
|
77
|
+
if (req.method === 'OPTIONS') {
|
|
78
|
+
res.writeHead(204, {
|
|
79
|
+
'Access-Control-Allow-Origin': '*',
|
|
80
|
+
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
|
81
|
+
'Access-Control-Allow-Headers': 'Content-Type',
|
|
82
|
+
});
|
|
83
|
+
res.end();
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
// GET /health
|
|
87
|
+
if (req.method === 'GET' && url.pathname === '/health') {
|
|
88
|
+
const clients = relay.listClients();
|
|
89
|
+
jsonResponse(res, 200, {
|
|
90
|
+
ok: true,
|
|
91
|
+
pluginConnected: relay.isPluginConnected(),
|
|
92
|
+
clients,
|
|
93
|
+
});
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
// GET /clients
|
|
97
|
+
if (req.method === 'GET' && url.pathname === '/clients') {
|
|
98
|
+
jsonResponse(res, 200, { clients: relay.listClients() });
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
// GET /tools
|
|
102
|
+
if (req.method === 'GET' && url.pathname === '/tools') {
|
|
103
|
+
jsonResponse(res, 200, { tools: toolCatalog });
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
// POST /tool/:name[?file=fileKey]
|
|
107
|
+
const toolMatch = url.pathname.match(/^\/tool\/([a-z_]+)$/);
|
|
108
|
+
if (req.method === 'POST' && toolMatch) {
|
|
109
|
+
const toolName = toolMatch[1];
|
|
110
|
+
const fileKey = url.searchParams.get('file') || undefined;
|
|
111
|
+
if (!toolSet.has(toolName)) {
|
|
112
|
+
jsonResponse(res, 404, { error: `Unknown tool: ${toolName}` });
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
// Require ?file= when multiple clients are connected
|
|
116
|
+
const clients = relay.listClients();
|
|
117
|
+
if (!fileKey && clients.length > 1) {
|
|
118
|
+
const fileList = clients.map(c => ` "${c.fileName}" (?file=${encodeURIComponent(c.fileName.replace('[Draft] ', ''))})`).join('\n');
|
|
119
|
+
jsonResponse(res, 400, {
|
|
120
|
+
error: `Multiple Figma files connected. Add ?file=<name> to target one:\n${fileList}`,
|
|
121
|
+
});
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (!relay.isPluginConnected()) {
|
|
125
|
+
jsonResponse(res, 503, {
|
|
126
|
+
error: 'Figma plugin is not connected. Open Figma and run the plugin first.',
|
|
127
|
+
});
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
try {
|
|
131
|
+
const body = await readBody(req);
|
|
132
|
+
const params = body ? JSON.parse(body) : {};
|
|
133
|
+
// Pit-of-success: screenshot=true implies mode:detail (screenshot only works in detail mode)
|
|
134
|
+
if (toolName === 'inspect' && params.screenshot && !params.mode) {
|
|
135
|
+
params.mode = 'detail';
|
|
136
|
+
}
|
|
137
|
+
// Route to specific file or any available
|
|
138
|
+
const response = fileKey
|
|
139
|
+
? await relay.callToolForFile(fileKey, toolName, params)
|
|
140
|
+
: await relay.callTool(toolName, params);
|
|
141
|
+
if (response.error) {
|
|
142
|
+
jsonResponse(res, 422, { error: response.error });
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
const data = response.data ?? response;
|
|
146
|
+
// Normalize __image → screenshot for cleaner API surface
|
|
147
|
+
if (data?.__image) {
|
|
148
|
+
data.screenshot = data.__image;
|
|
149
|
+
delete data.__image;
|
|
150
|
+
}
|
|
151
|
+
// Forward tool warnings (sizing demotes, dependency violations, ambiguous
|
|
152
|
+
// variable autopicks, etc.) — these mirror what the LLM sees and are
|
|
153
|
+
// load-bearing for debugging "why didn't my intent take" cases.
|
|
154
|
+
const out = { ok: true, data };
|
|
155
|
+
if (response.warnings && response.warnings.length > 0) {
|
|
156
|
+
out.warnings = response.warnings;
|
|
157
|
+
}
|
|
158
|
+
jsonResponse(res, 200, out);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
catch (err) {
|
|
162
|
+
const isTimeout = err.message?.includes('timed out');
|
|
163
|
+
const isNotConnected = err.message?.includes('not connected') || err.message?.includes('No Figma file');
|
|
164
|
+
jsonResponse(res, isTimeout ? 504 : isNotConnected ? 404 : 500, { error: err.message });
|
|
165
|
+
}
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
jsonResponse(res, 404, { error: 'Not found' });
|
|
169
|
+
});
|
|
170
|
+
server.listen(HTTP_PORT, () => {
|
|
171
|
+
console.error(`[HTTP Bridge] Listening on http://localhost:${HTTP_PORT}`);
|
|
172
|
+
console.error(`[HTTP Bridge] WebSocket relay on port ${WS_PORT}`);
|
|
173
|
+
console.error(`[HTTP Bridge] Endpoints:`);
|
|
174
|
+
console.error(`[HTTP Bridge] GET /health, /clients, /tools`);
|
|
175
|
+
console.error(`[HTTP Bridge] POST /tool/:name[?file=fileKey]`);
|
|
176
|
+
});
|
|
177
|
+
process.on('SIGINT', () => {
|
|
178
|
+
relay.close();
|
|
179
|
+
server.close();
|
|
180
|
+
process.exit(0);
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
main().catch((err) => {
|
|
184
|
+
console.error('[HTTP Bridge] Fatal error:', err);
|
|
185
|
+
process.exit(1);
|
|
186
|
+
});
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
/**
|
|
4
|
+
* @file MCP Server — Expose Figma plugin tools to external clients (Claude Code, etc.)
|
|
5
|
+
*
|
|
6
|
+
* Architecture:
|
|
7
|
+
* Claude Code (MCP client, stdio)
|
|
8
|
+
* → This MCP Server (Node.js, stdio transport)
|
|
9
|
+
* → WebSocket relay (localhost:3458) → Figma Plugin
|
|
10
|
+
*
|
|
11
|
+
* Tool schema is loaded from `tools-schema.json` (generated by `extract-schema.ts`
|
|
12
|
+
* at build time). This decouples the published `genable-mcp` npm package from
|
|
13
|
+
* the rest of the dogfood codebase — the package only ships compiled JS + JSON,
|
|
14
|
+
* not the plugin's TS source tree.
|
|
15
|
+
*
|
|
16
|
+
* All calls are relayed to the Figma plugin via WebSocket.
|
|
17
|
+
*
|
|
18
|
+
* IMPORTANT: Use console.error() for logging — stdout is reserved for MCP JSON-RPC.
|
|
19
|
+
*/
|
|
20
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
+
const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js");
|
|
22
|
+
const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
23
|
+
const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
|
|
24
|
+
const fs_1 = require("fs");
|
|
25
|
+
const path_1 = require("path");
|
|
26
|
+
const wsRelay_js_1 = require("./wsRelay.js");
|
|
27
|
+
function loadToolsSchema() {
|
|
28
|
+
const schemaPath = (0, path_1.join)(__dirname, 'tools-schema.json');
|
|
29
|
+
try {
|
|
30
|
+
return JSON.parse((0, fs_1.readFileSync)(schemaPath, 'utf-8'));
|
|
31
|
+
}
|
|
32
|
+
catch (err) {
|
|
33
|
+
console.error(`[MCP] FATAL: failed to load ${schemaPath}: ${err.message}`);
|
|
34
|
+
console.error('[MCP] Run `npx tsx tools/mcp-server/extract-schema.ts` to regenerate.');
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const unifiedTools = loadToolsSchema();
|
|
39
|
+
// ── Convert ToolDefinition.parameters → MCP inputSchema ──
|
|
40
|
+
function toMcpInputSchema(def) {
|
|
41
|
+
return {
|
|
42
|
+
type: 'object',
|
|
43
|
+
properties: def.parameters.properties,
|
|
44
|
+
required: def.parameters.required ?? [],
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
// ── Build MCP content from tool response ──
|
|
48
|
+
function buildMcpContent(response) {
|
|
49
|
+
// Error response
|
|
50
|
+
if (response.error) {
|
|
51
|
+
return {
|
|
52
|
+
content: [{ type: 'text', text: JSON.stringify({ error: response.error }) }],
|
|
53
|
+
isError: true,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
const data = response.data ?? response;
|
|
57
|
+
const content = [];
|
|
58
|
+
// Extract image if present (get_screenshot tool — data.__image)
|
|
59
|
+
if (data?.__image) {
|
|
60
|
+
const { __image, ...rest } = data;
|
|
61
|
+
content.push({ type: 'text', text: JSON.stringify(rest) });
|
|
62
|
+
content.push({
|
|
63
|
+
type: 'image',
|
|
64
|
+
data: __image.data,
|
|
65
|
+
mimeType: __image.mimeType,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
content.push({ type: 'text', text: JSON.stringify(data) });
|
|
70
|
+
}
|
|
71
|
+
return { content };
|
|
72
|
+
}
|
|
73
|
+
// ── Main ──
|
|
74
|
+
async function main() {
|
|
75
|
+
const relay = (0, wsRelay_js_1.createWsRelay)();
|
|
76
|
+
const server = new index_js_1.Server({ name: 'figma-ai-generator', version: '1.0.0' }, { capabilities: { tools: {} } });
|
|
77
|
+
// List tools — auto-convert from ToolDefinition
|
|
78
|
+
server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({
|
|
79
|
+
tools: unifiedTools.map((def) => ({
|
|
80
|
+
name: def.name,
|
|
81
|
+
description: def.description,
|
|
82
|
+
inputSchema: toMcpInputSchema(def),
|
|
83
|
+
})),
|
|
84
|
+
}));
|
|
85
|
+
// Call tool — relay to Figma plugin via WebSocket
|
|
86
|
+
server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
|
|
87
|
+
const { name: toolName, arguments: params = {} } = request.params;
|
|
88
|
+
if (!relay.isPluginConnected()) {
|
|
89
|
+
return {
|
|
90
|
+
content: [{
|
|
91
|
+
type: 'text',
|
|
92
|
+
text: 'Figma plugin is not connected. Open Figma and run the plugin first, then retry.',
|
|
93
|
+
}],
|
|
94
|
+
isError: true,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
try {
|
|
98
|
+
const response = await relay.callTool(toolName, params);
|
|
99
|
+
return buildMcpContent(response);
|
|
100
|
+
}
|
|
101
|
+
catch (err) {
|
|
102
|
+
return {
|
|
103
|
+
content: [{ type: 'text', text: err.message || String(err) }],
|
|
104
|
+
isError: true,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
// Connect stdio transport
|
|
109
|
+
const transport = new stdio_js_1.StdioServerTransport();
|
|
110
|
+
await server.connect(transport);
|
|
111
|
+
console.error('[MCP] Figma AI Generator MCP server running (stdio)');
|
|
112
|
+
// Graceful shutdown
|
|
113
|
+
process.on('SIGINT', () => {
|
|
114
|
+
relay.close();
|
|
115
|
+
process.exit(0);
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
main().catch((err) => {
|
|
119
|
+
console.error('[MCP] Fatal error:', err);
|
|
120
|
+
process.exit(1);
|
|
121
|
+
});
|