@sangheepark/figma-ds-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/dist/index.d.ts +2 -0
- package/dist/index.js +41 -0
- package/dist/tools/data-tools.d.ts +3 -0
- package/dist/tools/data-tools.js +107 -0
- package/dist/tools/mode-tools.d.ts +3 -0
- package/dist/tools/mode-tools.js +153 -0
- package/dist/tools/spec-tools.d.ts +3 -0
- package/dist/tools/spec-tools.js +189 -0
- package/dist/tools/style-tools.d.ts +3 -0
- package/dist/tools/style-tools.js +122 -0
- package/dist/tools/utility-tools.d.ts +3 -0
- package/dist/tools/utility-tools.js +92 -0
- package/dist/tools/variable-tools.d.ts +3 -0
- package/dist/tools/variable-tools.js +109 -0
- package/dist/ws-bridge.d.ts +22 -0
- package/dist/ws-bridge.js +110 -0
- package/package.json +42 -0
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Code-to-Figma MCP Server
|
|
4
|
+
*
|
|
5
|
+
* Bridges Claude Code (via MCP/stdio) to the Figma Bridge Plugin (via WebSocket).
|
|
6
|
+
* Provides tools for creating Figma components from JSON specs.
|
|
7
|
+
*/
|
|
8
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
9
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
10
|
+
import { FigmaBridge } from './ws-bridge.js';
|
|
11
|
+
import { registerUtilityTools } from './tools/utility-tools.js';
|
|
12
|
+
import { registerSpecTools } from './tools/spec-tools.js';
|
|
13
|
+
import { registerModeTools } from './tools/mode-tools.js';
|
|
14
|
+
import { registerVariableTools } from './tools/variable-tools.js';
|
|
15
|
+
import { registerStyleTools } from './tools/style-tools.js';
|
|
16
|
+
import { registerDataTools } from './tools/data-tools.js';
|
|
17
|
+
// Create the WebSocket bridge to Figma
|
|
18
|
+
const bridge = new FigmaBridge();
|
|
19
|
+
// Create MCP server
|
|
20
|
+
const server = new McpServer({
|
|
21
|
+
name: 'code-to-figma',
|
|
22
|
+
version: '0.1.0',
|
|
23
|
+
});
|
|
24
|
+
// Register all tools
|
|
25
|
+
registerUtilityTools(server, bridge);
|
|
26
|
+
registerSpecTools(server, bridge);
|
|
27
|
+
registerModeTools(server, bridge);
|
|
28
|
+
registerVariableTools(server, bridge);
|
|
29
|
+
registerStyleTools(server, bridge);
|
|
30
|
+
registerDataTools(server, bridge);
|
|
31
|
+
// Connect to Claude Code via stdio
|
|
32
|
+
async function main() {
|
|
33
|
+
const transport = new StdioServerTransport();
|
|
34
|
+
await server.connect(transport);
|
|
35
|
+
console.error('[MCP] Code-to-Figma MCP server started (stdio + WebSocket on port 3055)');
|
|
36
|
+
}
|
|
37
|
+
main().catch((error) => {
|
|
38
|
+
console.error('[MCP] Fatal error:', error);
|
|
39
|
+
process.exit(1);
|
|
40
|
+
});
|
|
41
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Data management tools: fetch_component_sets, export_style_guide, export_icons
|
|
3
|
+
* Phase 4 — Design system data update automation
|
|
4
|
+
*/
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
export function registerDataTools(server, bridge) {
|
|
7
|
+
// fetch_component_sets — Fetch component sets from Figma REST API (server-side, no bridge)
|
|
8
|
+
server.tool('fetch_component_sets', 'Fetch component sets from a Figma library file via REST API. Requires a Figma Personal Access Token (PAT) and file key. Returns component set names, keys, and node IDs.', {
|
|
9
|
+
fileKey: z.string().describe('Figma file key (from URL: figma.com/design/{fileKey}/...)'),
|
|
10
|
+
token: z.string().describe('Figma Personal Access Token (PAT)'),
|
|
11
|
+
}, async ({ fileKey, token }) => {
|
|
12
|
+
try {
|
|
13
|
+
const url = `https://api.figma.com/v1/files/${fileKey}/component_sets`;
|
|
14
|
+
const response = await fetch(url, {
|
|
15
|
+
headers: { 'X-Figma-Token': token },
|
|
16
|
+
});
|
|
17
|
+
if (!response.ok) {
|
|
18
|
+
const errorText = await response.text();
|
|
19
|
+
throw new Error(`Figma API error (${response.status}): ${errorText}`);
|
|
20
|
+
}
|
|
21
|
+
const data = await response.json();
|
|
22
|
+
const componentSets = data.meta?.component_sets || [];
|
|
23
|
+
// Return summary + full data
|
|
24
|
+
return {
|
|
25
|
+
content: [{
|
|
26
|
+
type: 'text',
|
|
27
|
+
text: JSON.stringify({
|
|
28
|
+
success: true,
|
|
29
|
+
message: `Fetched ${componentSets.length} component sets from file ${fileKey}`,
|
|
30
|
+
count: componentSets.length,
|
|
31
|
+
data: { meta: { component_sets: componentSets } },
|
|
32
|
+
}, null, 2),
|
|
33
|
+
}],
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
const msg = error instanceof Error ? error.message : 'Unknown error';
|
|
38
|
+
return {
|
|
39
|
+
content: [{
|
|
40
|
+
type: 'text',
|
|
41
|
+
text: JSON.stringify({ success: false, error: msg }, null, 2),
|
|
42
|
+
}],
|
|
43
|
+
isError: true,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
// export_style_guide — Export styles, variables, and components from the current Figma file
|
|
48
|
+
server.tool('export_style_guide', 'Export the style guide (variables, styles, components) from the currently open Figma file. Returns both JSON (for import/preset) and Markdown (for AI context). Requires the Bridge plugin to be connected.', {}, async () => {
|
|
49
|
+
try {
|
|
50
|
+
const result = await bridge.send('bridge-export-style-guide', {});
|
|
51
|
+
return {
|
|
52
|
+
content: [{
|
|
53
|
+
type: 'text',
|
|
54
|
+
text: JSON.stringify({
|
|
55
|
+
success: true,
|
|
56
|
+
message: `Exported style guide: ${result.variableCount} variables, ${result.styleCount} styles, ${result.componentCount} components, ${result.libraryComponentCount} library components`,
|
|
57
|
+
variableCount: result.variableCount,
|
|
58
|
+
styleCount: result.styleCount,
|
|
59
|
+
componentCount: result.componentCount,
|
|
60
|
+
libraryComponentCount: result.libraryComponentCount,
|
|
61
|
+
json: result.json,
|
|
62
|
+
markdown: result.markdown,
|
|
63
|
+
}, null, 2),
|
|
64
|
+
}],
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
const msg = error instanceof Error ? error.message : 'Unknown error';
|
|
69
|
+
return {
|
|
70
|
+
content: [{
|
|
71
|
+
type: 'text',
|
|
72
|
+
text: JSON.stringify({ success: false, error: msg }, null, 2),
|
|
73
|
+
}],
|
|
74
|
+
isError: true,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
// export_icons — Extract icon component keys from the current Figma page
|
|
79
|
+
server.tool('export_icons', 'Extract icon component names and published keys from instances on the current Figma page. Returns an array of { name, key } objects. Requires the Bridge plugin to be connected and the icon page to be open.', {}, async () => {
|
|
80
|
+
try {
|
|
81
|
+
const result = await bridge.send('bridge-export-icons', {});
|
|
82
|
+
const icons = (result.icons || []);
|
|
83
|
+
return {
|
|
84
|
+
content: [{
|
|
85
|
+
type: 'text',
|
|
86
|
+
text: JSON.stringify({
|
|
87
|
+
success: true,
|
|
88
|
+
message: `Exported ${icons.length} icon keys from current page`,
|
|
89
|
+
count: icons.length,
|
|
90
|
+
icons,
|
|
91
|
+
}, null, 2),
|
|
92
|
+
}],
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
const msg = error instanceof Error ? error.message : 'Unknown error';
|
|
97
|
+
return {
|
|
98
|
+
content: [{
|
|
99
|
+
type: 'text',
|
|
100
|
+
text: JSON.stringify({ success: false, error: msg }, null, 2),
|
|
101
|
+
}],
|
|
102
|
+
isError: true,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
//# sourceMappingURL=data-tools.js.map
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mode tools: set_style_mode, load_preset, get_presets
|
|
3
|
+
*/
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
export function registerModeTools(server, bridge) {
|
|
6
|
+
// set_style_mode — Set the style resolution mode for subsequent operations
|
|
7
|
+
server.tool('set_style_mode', `Set the style resolution mode for subsequent create operations.
|
|
8
|
+
- "hardcode": Use raw color/size values directly
|
|
9
|
+
- "variable": Bind to Figma variable tokens using {varName} syntax
|
|
10
|
+
- "library": Use library style references with @StyleName syntax and {varName} for variables
|
|
11
|
+
|
|
12
|
+
After setting "library" mode, you should also call load_preset to load a library preset.`, {
|
|
13
|
+
mode: z.enum(['hardcode', 'variable', 'library']).describe('Style resolution mode'),
|
|
14
|
+
}, async ({ mode }) => {
|
|
15
|
+
try {
|
|
16
|
+
const result = await bridge.send('bridge-set-style-mode', { mode });
|
|
17
|
+
return {
|
|
18
|
+
content: [{
|
|
19
|
+
type: 'text',
|
|
20
|
+
text: JSON.stringify({
|
|
21
|
+
success: true,
|
|
22
|
+
mode,
|
|
23
|
+
message: result.message || `Style mode set to "${mode}"`,
|
|
24
|
+
}, null, 2),
|
|
25
|
+
}],
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
const msg = error instanceof Error ? error.message : 'Unknown error';
|
|
30
|
+
return {
|
|
31
|
+
content: [{
|
|
32
|
+
type: 'text',
|
|
33
|
+
text: JSON.stringify({ success: false, error: msg }, null, 2),
|
|
34
|
+
}],
|
|
35
|
+
isError: true,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
// load_preset — Load a library preset (required for library mode)
|
|
40
|
+
server.tool('load_preset', `Load a library preset by ID. Use get_presets first to see available presets.
|
|
41
|
+
Loading a preset provides variable names, style names, and library component keys for use in specs.`, {
|
|
42
|
+
presetId: z.string().describe('Preset ID from get_presets list'),
|
|
43
|
+
}, async ({ presetId }) => {
|
|
44
|
+
try {
|
|
45
|
+
const result = await bridge.send('bridge-load-preset', { presetId });
|
|
46
|
+
return {
|
|
47
|
+
content: [{
|
|
48
|
+
type: 'text',
|
|
49
|
+
text: JSON.stringify({
|
|
50
|
+
success: true,
|
|
51
|
+
presetId: result.presetId,
|
|
52
|
+
libraryName: result.libraryName,
|
|
53
|
+
varCount: result.varCount,
|
|
54
|
+
styleCount: result.styleCount,
|
|
55
|
+
componentCount: result.componentCount,
|
|
56
|
+
}, null, 2),
|
|
57
|
+
}],
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
const msg = error instanceof Error ? error.message : 'Unknown error';
|
|
62
|
+
return {
|
|
63
|
+
content: [{
|
|
64
|
+
type: 'text',
|
|
65
|
+
text: JSON.stringify({ success: false, error: msg }, null, 2),
|
|
66
|
+
}],
|
|
67
|
+
isError: true,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
// get_presets — List available library presets
|
|
72
|
+
server.tool('get_presets', 'List all available library presets bundled in the plugin. Returns preset IDs, names, and descriptions.', {}, async () => {
|
|
73
|
+
try {
|
|
74
|
+
const result = await bridge.send('bridge-get-presets', {});
|
|
75
|
+
return {
|
|
76
|
+
content: [{
|
|
77
|
+
type: 'text',
|
|
78
|
+
text: JSON.stringify({
|
|
79
|
+
success: true,
|
|
80
|
+
presets: result.presets || [],
|
|
81
|
+
activePresetIds: result.activePresetIds || [],
|
|
82
|
+
primaryPresetId: result.primaryPresetId || null,
|
|
83
|
+
}, null, 2),
|
|
84
|
+
}],
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
const msg = error instanceof Error ? error.message : 'Unknown error';
|
|
89
|
+
return {
|
|
90
|
+
content: [{
|
|
91
|
+
type: 'text',
|
|
92
|
+
text: JSON.stringify({ success: false, error: msg }, null, 2),
|
|
93
|
+
}],
|
|
94
|
+
isError: true,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
// create_instance — Create a library/local component instance
|
|
99
|
+
server.tool('create_instance', `Create an instance of a library or local component in Figma.
|
|
100
|
+
|
|
101
|
+
For library components, provide library-key (from preset data).
|
|
102
|
+
For local components, provide component name.
|
|
103
|
+
|
|
104
|
+
Supports:
|
|
105
|
+
- variant selection: "Size=SM, Style=Primary"
|
|
106
|
+
- property overrides: { "Label": "Custom Text", "showIcon": true }
|
|
107
|
+
- text variable binding: { "Title": "i18n/greeting" }
|
|
108
|
+
- nested overrides: [{ name: "Icon", variant: "Type=Arrow" }]`, {
|
|
109
|
+
parentId: z.string().optional().describe('Parent nodeId to add instance to (optional — if omitted, placed on canvas)'),
|
|
110
|
+
component: z.string().optional().describe('Local component name (for local instances)'),
|
|
111
|
+
'library-key': z.string().optional().describe('Library component key (from preset data)'),
|
|
112
|
+
variant: z.string().optional().describe('Variant selection string, e.g. "Size=SM, Style=Primary"'),
|
|
113
|
+
overrides: z.record(z.union([z.string(), z.boolean()])).optional()
|
|
114
|
+
.describe('Property overrides: { "Label": "Custom", "showIcon": true }'),
|
|
115
|
+
'text-variables': z.record(z.string()).optional()
|
|
116
|
+
.describe('Text variable bindings: { "Title": "i18n/greeting" }'),
|
|
117
|
+
'nested-overrides': z.array(z.object({
|
|
118
|
+
name: z.string(),
|
|
119
|
+
index: z.number().optional(),
|
|
120
|
+
variant: z.string().optional(),
|
|
121
|
+
overrides: z.record(z.union([z.string(), z.boolean()])).optional(),
|
|
122
|
+
'text-variables': z.record(z.string()).optional(),
|
|
123
|
+
})).optional()
|
|
124
|
+
.describe('Override child instances within the parent instance'),
|
|
125
|
+
layout: z.record(z.unknown()).optional().describe('Optional layout overrides'),
|
|
126
|
+
style: z.record(z.unknown()).optional().describe('Optional style overrides'),
|
|
127
|
+
}, async (params) => {
|
|
128
|
+
try {
|
|
129
|
+
const result = await bridge.send('bridge-create-instance', params);
|
|
130
|
+
return {
|
|
131
|
+
content: [{
|
|
132
|
+
type: 'text',
|
|
133
|
+
text: JSON.stringify({
|
|
134
|
+
success: true,
|
|
135
|
+
nodeId: result.nodeId,
|
|
136
|
+
name: result.name,
|
|
137
|
+
}, null, 2),
|
|
138
|
+
}],
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
const msg = error instanceof Error ? error.message : 'Unknown error';
|
|
143
|
+
return {
|
|
144
|
+
content: [{
|
|
145
|
+
type: 'text',
|
|
146
|
+
text: JSON.stringify({ success: false, error: msg }, null, 2),
|
|
147
|
+
}],
|
|
148
|
+
isError: true,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
//# sourceMappingURL=mode-tools.js.map
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spec tools: create_from_spec, create_node, add_child, modify_node
|
|
3
|
+
*/
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
// Reusable schema fragments
|
|
6
|
+
const layoutSchema = z.object({
|
|
7
|
+
direction: z.enum(['row', 'column']).optional(),
|
|
8
|
+
gap: z.string().optional(),
|
|
9
|
+
padding: z.string().optional(),
|
|
10
|
+
'align-items': z.enum(['start', 'flex-start', 'center', 'end', 'flex-end', 'stretch', 'baseline']).optional(),
|
|
11
|
+
'justify-content': z.enum(['start', 'flex-start', 'center', 'end', 'flex-end', 'space-between']).optional(),
|
|
12
|
+
width: z.string().optional(),
|
|
13
|
+
height: z.string().optional(),
|
|
14
|
+
positioning: z.enum(['auto', 'absolute']).optional(),
|
|
15
|
+
x: z.string().optional(),
|
|
16
|
+
y: z.string().optional(),
|
|
17
|
+
wrap: z.boolean().optional(),
|
|
18
|
+
'wrap-gap': z.string().optional(),
|
|
19
|
+
'z-order': z.enum(['first-on-top', 'last-on-top']).optional(),
|
|
20
|
+
'clip-content': z.boolean().optional(),
|
|
21
|
+
rotation: z.number().optional(),
|
|
22
|
+
'min-width': z.string().optional(),
|
|
23
|
+
'max-width': z.string().optional(),
|
|
24
|
+
'min-height': z.string().optional(),
|
|
25
|
+
'max-height': z.string().optional(),
|
|
26
|
+
}).passthrough().optional().describe('Layout properties (CSS flexbox terms)');
|
|
27
|
+
const styleSchema = z.object({
|
|
28
|
+
background: z.string().optional(),
|
|
29
|
+
'border-radius': z.string().optional(),
|
|
30
|
+
border: z.string().optional(),
|
|
31
|
+
'border-top': z.string().optional(),
|
|
32
|
+
'border-bottom': z.string().optional(),
|
|
33
|
+
'border-left': z.string().optional(),
|
|
34
|
+
'border-right': z.string().optional(),
|
|
35
|
+
opacity: z.number().optional(),
|
|
36
|
+
'font-family': z.string().optional(),
|
|
37
|
+
'font-size': z.string().optional(),
|
|
38
|
+
'font-weight': z.string().optional(),
|
|
39
|
+
color: z.string().optional(),
|
|
40
|
+
'text-align': z.enum(['left', 'center', 'right', 'justify']).optional(),
|
|
41
|
+
'line-height': z.string().optional(),
|
|
42
|
+
'letter-spacing': z.string().optional(),
|
|
43
|
+
'text-style': z.string().optional(),
|
|
44
|
+
'effect-style': z.string().optional(),
|
|
45
|
+
}).passthrough().optional().describe('Style properties (CSS terms). Use {varName} for variable tokens, @StyleName for library styles.');
|
|
46
|
+
export function registerSpecTools(server, bridge) {
|
|
47
|
+
// create_from_spec — Full spec in one shot (JSON, not YAML)
|
|
48
|
+
server.tool('create_from_spec', `Create a Figma component/frame/component-set from a complete JSON spec. This is the primary tool for creating Figma components.
|
|
49
|
+
|
|
50
|
+
The spec follows the same structure as the YAML format but in JSON:
|
|
51
|
+
- name (required): Component name
|
|
52
|
+
- type: "frame" | "component" | "component-set"
|
|
53
|
+
- layout: { direction, gap, padding, align-items, justify-content, width, height, ... }
|
|
54
|
+
- style: { background, border-radius, color, font-size, ... }
|
|
55
|
+
- children: Array of child node specs
|
|
56
|
+
- variants: Array of variant specs (for component-set)
|
|
57
|
+
- properties: Component property definitions
|
|
58
|
+
|
|
59
|
+
Style values can use:
|
|
60
|
+
- {variableName} for variable token binding
|
|
61
|
+
- @StyleName for library style references
|
|
62
|
+
|
|
63
|
+
If a component with the same name already exists, it will be updated in-place.`, {
|
|
64
|
+
spec: z.record(z.unknown()).describe('The complete RootSpec object (name, type, layout, style, children, variants, properties)'),
|
|
65
|
+
styleMode: z.enum(['hardcode', 'variable', 'library']).optional()
|
|
66
|
+
.describe('Style resolution mode. If omitted, uses the session style mode set by set_style_mode.'),
|
|
67
|
+
}, async ({ spec, styleMode }) => {
|
|
68
|
+
try {
|
|
69
|
+
const result = await bridge.send('bridge-create-from-spec', { spec, styleMode });
|
|
70
|
+
return {
|
|
71
|
+
content: [{
|
|
72
|
+
type: 'text',
|
|
73
|
+
text: JSON.stringify({
|
|
74
|
+
success: true,
|
|
75
|
+
message: result.message || 'Created successfully',
|
|
76
|
+
nodeId: result.nodeId,
|
|
77
|
+
name: result.name,
|
|
78
|
+
warnings: result.warnings,
|
|
79
|
+
}, null, 2),
|
|
80
|
+
}],
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
const msg = error instanceof Error ? error.message : 'Unknown error';
|
|
85
|
+
return {
|
|
86
|
+
content: [{
|
|
87
|
+
type: 'text',
|
|
88
|
+
text: JSON.stringify({ success: false, error: msg }, null, 2),
|
|
89
|
+
}],
|
|
90
|
+
isError: true,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
// create_node — Create a single empty node (for incremental building)
|
|
95
|
+
server.tool('create_node', `Create a single Figma node (frame or component) without children. Returns nodeId for use with add_child.
|
|
96
|
+
Use this for incremental/step-by-step component building.`, {
|
|
97
|
+
type: z.enum(['frame', 'component']).describe('Node type'),
|
|
98
|
+
name: z.string().describe('Node name'),
|
|
99
|
+
layout: layoutSchema,
|
|
100
|
+
style: styleSchema,
|
|
101
|
+
}, async ({ type, name, layout, style }) => {
|
|
102
|
+
try {
|
|
103
|
+
const result = await bridge.send('bridge-create-node', { type, name, layout, style });
|
|
104
|
+
return {
|
|
105
|
+
content: [{
|
|
106
|
+
type: 'text',
|
|
107
|
+
text: JSON.stringify({
|
|
108
|
+
success: true,
|
|
109
|
+
nodeId: result.nodeId,
|
|
110
|
+
name: result.name,
|
|
111
|
+
}, null, 2),
|
|
112
|
+
}],
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
const msg = error instanceof Error ? error.message : 'Unknown error';
|
|
117
|
+
return {
|
|
118
|
+
content: [{
|
|
119
|
+
type: 'text',
|
|
120
|
+
text: JSON.stringify({ success: false, error: msg }, null, 2),
|
|
121
|
+
}],
|
|
122
|
+
isError: true,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
// add_child — Add a child node to an existing parent
|
|
127
|
+
server.tool('add_child', `Add a child node to an existing parent frame/component. The child spec follows the ChildNodeSpec format:
|
|
128
|
+
- type: "frame" | "text" | "rectangle" | "instance" | "vector"
|
|
129
|
+
- name, content (text), layout, style, children, etc.
|
|
130
|
+
- For instances: component, library-key, variant, overrides, text-variables, nested-overrides`, {
|
|
131
|
+
parentId: z.string().describe('The nodeId of the parent frame/component'),
|
|
132
|
+
child: z.record(z.unknown()).describe('ChildNodeSpec object: { type, name, content, layout, style, children, ... }'),
|
|
133
|
+
}, async ({ parentId, child }) => {
|
|
134
|
+
try {
|
|
135
|
+
const result = await bridge.send('bridge-add-child', { parentId, child });
|
|
136
|
+
return {
|
|
137
|
+
content: [{
|
|
138
|
+
type: 'text',
|
|
139
|
+
text: JSON.stringify({
|
|
140
|
+
success: true,
|
|
141
|
+
nodeId: result.nodeId,
|
|
142
|
+
name: result.name,
|
|
143
|
+
}, null, 2),
|
|
144
|
+
}],
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
const msg = error instanceof Error ? error.message : 'Unknown error';
|
|
149
|
+
return {
|
|
150
|
+
content: [{
|
|
151
|
+
type: 'text',
|
|
152
|
+
text: JSON.stringify({ success: false, error: msg }, null, 2),
|
|
153
|
+
}],
|
|
154
|
+
isError: true,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
// modify_node — Modify an existing node's layout/style
|
|
159
|
+
server.tool('modify_node', 'Modify the layout and/or style of an existing Figma node by its nodeId.', {
|
|
160
|
+
nodeId: z.string().describe('The nodeId of the node to modify'),
|
|
161
|
+
layout: layoutSchema,
|
|
162
|
+
style: styleSchema,
|
|
163
|
+
}, async ({ nodeId, layout, style }) => {
|
|
164
|
+
try {
|
|
165
|
+
const result = await bridge.send('bridge-modify-node', { nodeId, layout, style });
|
|
166
|
+
return {
|
|
167
|
+
content: [{
|
|
168
|
+
type: 'text',
|
|
169
|
+
text: JSON.stringify({
|
|
170
|
+
success: true,
|
|
171
|
+
nodeId: result.nodeId,
|
|
172
|
+
message: result.message || 'Node modified',
|
|
173
|
+
}, null, 2),
|
|
174
|
+
}],
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
catch (error) {
|
|
178
|
+
const msg = error instanceof Error ? error.message : 'Unknown error';
|
|
179
|
+
return {
|
|
180
|
+
content: [{
|
|
181
|
+
type: 'text',
|
|
182
|
+
text: JSON.stringify({ success: false, error: msg }, null, 2),
|
|
183
|
+
}],
|
|
184
|
+
isError: true,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
//# sourceMappingURL=spec-tools.js.map
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Style tools: create_text_styles, create_effect_styles
|
|
3
|
+
*/
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
export function registerStyleTools(server, bridge) {
|
|
6
|
+
// create_text_styles — Create Text Styles
|
|
7
|
+
server.tool('create_text_styles', `Create Figma Text Styles.
|
|
8
|
+
|
|
9
|
+
Example:
|
|
10
|
+
{
|
|
11
|
+
"name": "Typography",
|
|
12
|
+
"font-family": "Inter",
|
|
13
|
+
"styles": [
|
|
14
|
+
{ "name": "Heading/H1", "font-size": "48px", "font-weight": 700, "line-height": "120%" },
|
|
15
|
+
{ "name": "Body/MD", "font-size": "16px", "font-weight": 400, "line-height": "150%" }
|
|
16
|
+
]
|
|
17
|
+
}`, {
|
|
18
|
+
name: z.string().describe('Style group name'),
|
|
19
|
+
'font-family': z.string().optional().describe('Default font family for all styles'),
|
|
20
|
+
styles: z.array(z.object({
|
|
21
|
+
name: z.string(),
|
|
22
|
+
'font-family': z.string().optional(),
|
|
23
|
+
'font-size': z.string().describe('e.g. "16px"'),
|
|
24
|
+
'font-weight': z.number().optional(),
|
|
25
|
+
'line-height': z.string().optional().describe('e.g. "150%", "24px", "auto"'),
|
|
26
|
+
'letter-spacing': z.string().optional(),
|
|
27
|
+
'text-decoration': z.enum(['none', 'underline', 'strikethrough']).optional(),
|
|
28
|
+
'text-case': z.enum(['original', 'uppercase', 'lowercase', 'capitalize']).optional(),
|
|
29
|
+
description: z.string().optional(),
|
|
30
|
+
})).describe('Text style definitions'),
|
|
31
|
+
}, async (params) => {
|
|
32
|
+
try {
|
|
33
|
+
const spec = {
|
|
34
|
+
type: 'text-styles',
|
|
35
|
+
...params,
|
|
36
|
+
};
|
|
37
|
+
const result = await bridge.send('bridge-create-text-styles', { spec });
|
|
38
|
+
return {
|
|
39
|
+
content: [{
|
|
40
|
+
type: 'text',
|
|
41
|
+
text: JSON.stringify({
|
|
42
|
+
success: true,
|
|
43
|
+
styleCount: result.styleCount,
|
|
44
|
+
createdCount: result.createdCount,
|
|
45
|
+
updatedCount: result.updatedCount,
|
|
46
|
+
message: result.message,
|
|
47
|
+
}, null, 2),
|
|
48
|
+
}],
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
const msg = error instanceof Error ? error.message : 'Unknown error';
|
|
53
|
+
return {
|
|
54
|
+
content: [{
|
|
55
|
+
type: 'text',
|
|
56
|
+
text: JSON.stringify({ success: false, error: msg }, null, 2),
|
|
57
|
+
}],
|
|
58
|
+
isError: true,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
// create_effect_styles — Create Effect Styles
|
|
63
|
+
server.tool('create_effect_styles', `Create Figma Effect Styles (shadows, blurs).
|
|
64
|
+
|
|
65
|
+
Example:
|
|
66
|
+
{
|
|
67
|
+
"name": "Effects",
|
|
68
|
+
"styles": [
|
|
69
|
+
{
|
|
70
|
+
"name": "Shadow/MD",
|
|
71
|
+
"effects": [
|
|
72
|
+
{ "type": "drop-shadow", "x": 0, "y": 4, "blur": 6, "spread": -1, "color": "rgba(0,0,0,0.1)" }
|
|
73
|
+
]
|
|
74
|
+
}
|
|
75
|
+
]
|
|
76
|
+
}`, {
|
|
77
|
+
name: z.string().describe('Style group name'),
|
|
78
|
+
styles: z.array(z.object({
|
|
79
|
+
name: z.string(),
|
|
80
|
+
effects: z.array(z.object({
|
|
81
|
+
type: z.enum(['drop-shadow', 'inner-shadow', 'layer-blur', 'background-blur']),
|
|
82
|
+
x: z.number().optional(),
|
|
83
|
+
y: z.number().optional(),
|
|
84
|
+
blur: z.number(),
|
|
85
|
+
spread: z.number().optional(),
|
|
86
|
+
color: z.string().optional(),
|
|
87
|
+
})).describe('Effect definitions'),
|
|
88
|
+
description: z.string().optional(),
|
|
89
|
+
})).describe('Effect style definitions'),
|
|
90
|
+
}, async (params) => {
|
|
91
|
+
try {
|
|
92
|
+
const spec = {
|
|
93
|
+
type: 'effect-styles',
|
|
94
|
+
...params,
|
|
95
|
+
};
|
|
96
|
+
const result = await bridge.send('bridge-create-effect-styles', { spec });
|
|
97
|
+
return {
|
|
98
|
+
content: [{
|
|
99
|
+
type: 'text',
|
|
100
|
+
text: JSON.stringify({
|
|
101
|
+
success: true,
|
|
102
|
+
styleCount: result.styleCount,
|
|
103
|
+
createdCount: result.createdCount,
|
|
104
|
+
updatedCount: result.updatedCount,
|
|
105
|
+
message: result.message,
|
|
106
|
+
}, null, 2),
|
|
107
|
+
}],
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
const msg = error instanceof Error ? error.message : 'Unknown error';
|
|
112
|
+
return {
|
|
113
|
+
content: [{
|
|
114
|
+
type: 'text',
|
|
115
|
+
text: JSON.stringify({ success: false, error: msg }, null, 2),
|
|
116
|
+
}],
|
|
117
|
+
isError: true,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
//# sourceMappingURL=style-tools.js.map
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility tools: get_status, validate_spec, generate_from_yaml
|
|
3
|
+
*/
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
export function registerUtilityTools(server, bridge) {
|
|
6
|
+
// get_status — Check connection status
|
|
7
|
+
server.tool('get_status', 'Check if the Figma Bridge plugin is connected and ready', {}, async () => {
|
|
8
|
+
const connected = bridge.isConnected();
|
|
9
|
+
return {
|
|
10
|
+
content: [{
|
|
11
|
+
type: 'text',
|
|
12
|
+
text: JSON.stringify({
|
|
13
|
+
connected,
|
|
14
|
+
message: connected
|
|
15
|
+
? 'Figma Bridge plugin is connected and ready.'
|
|
16
|
+
: 'Figma Bridge plugin is NOT connected. Please open the "Code to Figma Bridge" plugin in Figma.',
|
|
17
|
+
}, null, 2),
|
|
18
|
+
}],
|
|
19
|
+
};
|
|
20
|
+
});
|
|
21
|
+
// validate_spec — Validate a JSON spec without creating anything
|
|
22
|
+
server.tool('validate_spec', 'Validate a component/variable/style spec (JSON) without creating anything in Figma. Returns validation errors if any.', {
|
|
23
|
+
spec: z.record(z.unknown()).describe('The spec object to validate (same format as create_from_spec)'),
|
|
24
|
+
}, async ({ spec }) => {
|
|
25
|
+
// We convert the spec to YAML-like format and send it to the plugin's validate handler
|
|
26
|
+
// Actually, we send it as a bridge-validate-spec message that the plugin will handle
|
|
27
|
+
try {
|
|
28
|
+
const result = await bridge.send('bridge-validate-spec', { spec });
|
|
29
|
+
return {
|
|
30
|
+
content: [{
|
|
31
|
+
type: 'text',
|
|
32
|
+
text: JSON.stringify({
|
|
33
|
+
valid: true,
|
|
34
|
+
specType: result.specType || 'node',
|
|
35
|
+
message: result.message || 'Spec is valid',
|
|
36
|
+
}, null, 2),
|
|
37
|
+
}],
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
const msg = error instanceof Error ? error.message : 'Unknown error';
|
|
42
|
+
return {
|
|
43
|
+
content: [{
|
|
44
|
+
type: 'text',
|
|
45
|
+
text: JSON.stringify({
|
|
46
|
+
valid: false,
|
|
47
|
+
error: msg,
|
|
48
|
+
}, null, 2),
|
|
49
|
+
}],
|
|
50
|
+
isError: true,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
// generate_from_yaml — Legacy YAML passthrough for compatibility
|
|
55
|
+
server.tool('generate_from_yaml', 'Generate Figma components from YAML text (legacy passthrough). Prefer using create_from_spec with JSON instead.', {
|
|
56
|
+
yaml: z.string().describe('YAML spec text'),
|
|
57
|
+
styleMode: z.enum(['hardcode', 'variable', 'library']).optional().default('hardcode')
|
|
58
|
+
.describe('Style resolution mode: hardcode (raw values), variable (token binding), library (library style refs)'),
|
|
59
|
+
dryRun: z.boolean().optional().default(false)
|
|
60
|
+
.describe('If true, preview what would be created without actually creating'),
|
|
61
|
+
}, async ({ yaml, styleMode, dryRun }) => {
|
|
62
|
+
try {
|
|
63
|
+
const result = await bridge.send('generate', {
|
|
64
|
+
yaml,
|
|
65
|
+
styleMode,
|
|
66
|
+
collectionId: styleMode === 'variable' ? 'all' : null,
|
|
67
|
+
dryRun,
|
|
68
|
+
});
|
|
69
|
+
return {
|
|
70
|
+
content: [{
|
|
71
|
+
type: 'text',
|
|
72
|
+
text: JSON.stringify({
|
|
73
|
+
success: true,
|
|
74
|
+
message: result.message || 'Generated successfully',
|
|
75
|
+
data: result,
|
|
76
|
+
}, null, 2),
|
|
77
|
+
}],
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
const msg = error instanceof Error ? error.message : 'Unknown error';
|
|
82
|
+
return {
|
|
83
|
+
content: [{
|
|
84
|
+
type: 'text',
|
|
85
|
+
text: JSON.stringify({ success: false, error: msg }, null, 2),
|
|
86
|
+
}],
|
|
87
|
+
isError: true,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
//# sourceMappingURL=utility-tools.js.map
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Variable tools: create_variables, create_i18n_variables
|
|
3
|
+
*/
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
export function registerVariableTools(server, bridge) {
|
|
6
|
+
// create_variables — Create a Variable Collection
|
|
7
|
+
server.tool('create_variables', `Create a Figma Variable Collection with multi-mode support.
|
|
8
|
+
|
|
9
|
+
Example:
|
|
10
|
+
{
|
|
11
|
+
"name": "Colors",
|
|
12
|
+
"modes": ["Light", "Dark"],
|
|
13
|
+
"variables": [
|
|
14
|
+
{ "name": "brand/primary", "type": "COLOR", "values": { "Light": "#3B82F6", "Dark": "#60A5FA" } },
|
|
15
|
+
{ "name": "spacing/md", "type": "FLOAT", "values": { "Light": "16", "Dark": "16" } }
|
|
16
|
+
]
|
|
17
|
+
}`, {
|
|
18
|
+
name: z.string().describe('Collection name'),
|
|
19
|
+
modes: z.array(z.string()).min(1).describe('Mode names, e.g. ["Light", "Dark"]'),
|
|
20
|
+
description: z.string().optional(),
|
|
21
|
+
variables: z.array(z.object({
|
|
22
|
+
name: z.string(),
|
|
23
|
+
type: z.enum(['COLOR', 'FLOAT', 'STRING', 'BOOLEAN']),
|
|
24
|
+
values: z.record(z.string()).describe('Mode name → value mapping'),
|
|
25
|
+
description: z.string().optional(),
|
|
26
|
+
scopes: z.array(z.string()).optional(),
|
|
27
|
+
})).describe('Variable definitions'),
|
|
28
|
+
}, async (params) => {
|
|
29
|
+
try {
|
|
30
|
+
const spec = {
|
|
31
|
+
type: 'variable-collection',
|
|
32
|
+
...params,
|
|
33
|
+
};
|
|
34
|
+
const result = await bridge.send('bridge-create-variables', { spec });
|
|
35
|
+
return {
|
|
36
|
+
content: [{
|
|
37
|
+
type: 'text',
|
|
38
|
+
text: JSON.stringify({
|
|
39
|
+
success: true,
|
|
40
|
+
collectionName: result.collectionName,
|
|
41
|
+
variableCount: result.variableCount,
|
|
42
|
+
modeCount: result.modeCount,
|
|
43
|
+
message: result.message,
|
|
44
|
+
}, null, 2),
|
|
45
|
+
}],
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
const msg = error instanceof Error ? error.message : 'Unknown error';
|
|
50
|
+
return {
|
|
51
|
+
content: [{
|
|
52
|
+
type: 'text',
|
|
53
|
+
text: JSON.stringify({ success: false, error: msg }, null, 2),
|
|
54
|
+
}],
|
|
55
|
+
isError: true,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
// create_i18n_variables — Create/update i18n STRING variables
|
|
60
|
+
server.tool('create_i18n_variables', `Create or update i18n STRING variables in a Figma Variable Collection.
|
|
61
|
+
|
|
62
|
+
Example:
|
|
63
|
+
{
|
|
64
|
+
"collection": "Translations",
|
|
65
|
+
"modes": { "Eng": "en", "Kor": "ko" },
|
|
66
|
+
"data": {
|
|
67
|
+
"greeting": { "en": "Hello", "ko": "안녕하세요" },
|
|
68
|
+
"farewell": { "en": "Goodbye", "ko": "안녕히 가세요" }
|
|
69
|
+
}
|
|
70
|
+
}`, {
|
|
71
|
+
collection: z.string().describe('Variable Collection name'),
|
|
72
|
+
modes: z.record(z.string()).describe('Figma mode name → input key mapping, e.g. { "Eng": "en", "Kor": "ko" }'),
|
|
73
|
+
'skip-same-value': z.boolean().optional().default(true)
|
|
74
|
+
.describe('Skip variables where all mode values are identical'),
|
|
75
|
+
delete: z.array(z.string()).optional()
|
|
76
|
+
.describe('Variable names to delete from collection'),
|
|
77
|
+
data: z.record(z.record(z.string()))
|
|
78
|
+
.describe('Variable data: { variableName: { modeKey: value } }'),
|
|
79
|
+
}, async (params) => {
|
|
80
|
+
try {
|
|
81
|
+
const spec = {
|
|
82
|
+
type: 'i18n',
|
|
83
|
+
...params,
|
|
84
|
+
};
|
|
85
|
+
const result = await bridge.send('bridge-create-i18n-variables', { spec });
|
|
86
|
+
return {
|
|
87
|
+
content: [{
|
|
88
|
+
type: 'text',
|
|
89
|
+
text: JSON.stringify({
|
|
90
|
+
success: true,
|
|
91
|
+
message: result.message,
|
|
92
|
+
data: result,
|
|
93
|
+
}, null, 2),
|
|
94
|
+
}],
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
const msg = error instanceof Error ? error.message : 'Unknown error';
|
|
99
|
+
return {
|
|
100
|
+
content: [{
|
|
101
|
+
type: 'text',
|
|
102
|
+
text: JSON.stringify({ success: false, error: msg }, null, 2),
|
|
103
|
+
}],
|
|
104
|
+
isError: true,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
//# sourceMappingURL=variable-tools.js.map
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export declare class FigmaBridge {
|
|
2
|
+
private wss;
|
|
3
|
+
private client;
|
|
4
|
+
private pendingRequests;
|
|
5
|
+
private requestCounter;
|
|
6
|
+
constructor();
|
|
7
|
+
/**
|
|
8
|
+
* Check if Figma plugin is connected
|
|
9
|
+
*/
|
|
10
|
+
isConnected(): boolean;
|
|
11
|
+
/**
|
|
12
|
+
* Send a message to the Figma plugin and wait for a response.
|
|
13
|
+
* Returns the response data or throws on timeout/error.
|
|
14
|
+
*/
|
|
15
|
+
send(type: string, payload?: Record<string, unknown>): Promise<Record<string, unknown>>;
|
|
16
|
+
/**
|
|
17
|
+
* Shut down the WebSocket server
|
|
18
|
+
*/
|
|
19
|
+
close(): void;
|
|
20
|
+
private handleMessage;
|
|
21
|
+
private nextRequestId;
|
|
22
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebSocket Bridge — manages connection to Figma Bridge Plugin.
|
|
3
|
+
* Provides request/response correlation via requestId.
|
|
4
|
+
*/
|
|
5
|
+
import { WebSocketServer, WebSocket } from 'ws';
|
|
6
|
+
const WS_PORT = 3055;
|
|
7
|
+
const REQUEST_TIMEOUT = 180_000; // 180 seconds (font/library loading can be very slow)
|
|
8
|
+
export class FigmaBridge {
|
|
9
|
+
wss;
|
|
10
|
+
client = null;
|
|
11
|
+
pendingRequests = new Map();
|
|
12
|
+
requestCounter = 0;
|
|
13
|
+
constructor() {
|
|
14
|
+
this.wss = new WebSocketServer({ port: WS_PORT });
|
|
15
|
+
this.wss.on('connection', (ws) => {
|
|
16
|
+
console.error(`[Bridge] Figma plugin connected`);
|
|
17
|
+
this.client = ws;
|
|
18
|
+
ws.on('message', (data) => {
|
|
19
|
+
try {
|
|
20
|
+
const msg = JSON.parse(data.toString());
|
|
21
|
+
this.handleMessage(msg);
|
|
22
|
+
}
|
|
23
|
+
catch (e) {
|
|
24
|
+
console.error('[Bridge] Failed to parse message:', e);
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
ws.on('close', () => {
|
|
28
|
+
console.error('[Bridge] Figma plugin disconnected');
|
|
29
|
+
if (this.client === ws) {
|
|
30
|
+
this.client = null;
|
|
31
|
+
}
|
|
32
|
+
// Reject all pending requests
|
|
33
|
+
for (const [id, pending] of this.pendingRequests) {
|
|
34
|
+
clearTimeout(pending.timer);
|
|
35
|
+
pending.reject(new Error('Figma plugin disconnected'));
|
|
36
|
+
this.pendingRequests.delete(id);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
ws.on('error', (err) => {
|
|
40
|
+
console.error('[Bridge] WebSocket error:', err.message);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
this.wss.on('error', (err) => {
|
|
44
|
+
console.error('[Bridge] WebSocket server error:', err.message);
|
|
45
|
+
});
|
|
46
|
+
console.error(`[Bridge] WebSocket server listening on port ${WS_PORT}`);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Check if Figma plugin is connected
|
|
50
|
+
*/
|
|
51
|
+
isConnected() {
|
|
52
|
+
return this.client !== null && this.client.readyState === WebSocket.OPEN;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Send a message to the Figma plugin and wait for a response.
|
|
56
|
+
* Returns the response data or throws on timeout/error.
|
|
57
|
+
*/
|
|
58
|
+
async send(type, payload = {}) {
|
|
59
|
+
if (!this.isConnected()) {
|
|
60
|
+
throw new Error('Figma Bridge plugin is not connected. Please open the "Code to Figma Bridge" plugin in Figma.');
|
|
61
|
+
}
|
|
62
|
+
const requestId = this.nextRequestId();
|
|
63
|
+
return new Promise((resolve, reject) => {
|
|
64
|
+
const timer = setTimeout(() => {
|
|
65
|
+
this.pendingRequests.delete(requestId);
|
|
66
|
+
reject(new Error(`Request timed out after ${REQUEST_TIMEOUT / 1000}s (type: ${type})`));
|
|
67
|
+
}, REQUEST_TIMEOUT);
|
|
68
|
+
this.pendingRequests.set(requestId, { resolve, reject, timer });
|
|
69
|
+
const message = JSON.stringify({
|
|
70
|
+
requestId,
|
|
71
|
+
type,
|
|
72
|
+
params: payload,
|
|
73
|
+
});
|
|
74
|
+
this.client.send(message);
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Shut down the WebSocket server
|
|
79
|
+
*/
|
|
80
|
+
close() {
|
|
81
|
+
this.wss.close();
|
|
82
|
+
}
|
|
83
|
+
handleMessage(msg) {
|
|
84
|
+
// Handshake
|
|
85
|
+
if (msg.type === 'handshake') {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
// Bridge response — correlate with pending request
|
|
89
|
+
if (msg.type === 'bridge-response' && typeof msg.requestId === 'string') {
|
|
90
|
+
const pending = this.pendingRequests.get(msg.requestId);
|
|
91
|
+
if (pending) {
|
|
92
|
+
clearTimeout(pending.timer);
|
|
93
|
+
this.pendingRequests.delete(msg.requestId);
|
|
94
|
+
if (msg.success) {
|
|
95
|
+
pending.resolve(msg.data || msg);
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
pending.reject(new Error(msg.error || 'Unknown error from Figma'));
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
// Unknown message
|
|
104
|
+
console.error('[Bridge] Unhandled message type:', msg.type);
|
|
105
|
+
}
|
|
106
|
+
nextRequestId() {
|
|
107
|
+
return `req_${++this.requestCounter}_${Date.now()}`;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
//# sourceMappingURL=ws-bridge.js.map
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@sangheepark/figma-ds-mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "MCP server for Code to Figma Bridge — bridges Claude Code to Figma plugin via WebSocket",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"figma-ds-mcp": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist/**/*.js",
|
|
12
|
+
"dist/**/*.d.ts",
|
|
13
|
+
"!dist/bundle.js"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "tsc",
|
|
17
|
+
"prepublishOnly": "tsc",
|
|
18
|
+
"start": "node dist/index.js",
|
|
19
|
+
"dev": "tsc && node dist/index.js"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"mcp",
|
|
23
|
+
"figma",
|
|
24
|
+
"model-context-protocol",
|
|
25
|
+
"code-to-figma"
|
|
26
|
+
],
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "https://github.com/sagheepark/code-to-figma"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
34
|
+
"ws": "^8.16.0",
|
|
35
|
+
"zod": "^3.22.0"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@types/ws": "^8.5.10",
|
|
39
|
+
"@types/node": "^20.0.0",
|
|
40
|
+
"typescript": "^5.3.0"
|
|
41
|
+
}
|
|
42
|
+
}
|