@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.
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
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,3 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { FigmaBridge } from '../ws-bridge.js';
3
+ export declare function registerDataTools(server: McpServer, bridge: FigmaBridge): void;
@@ -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,3 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { FigmaBridge } from '../ws-bridge.js';
3
+ export declare function registerModeTools(server: McpServer, bridge: FigmaBridge): void;
@@ -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,3 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { FigmaBridge } from '../ws-bridge.js';
3
+ export declare function registerSpecTools(server: McpServer, bridge: FigmaBridge): void;
@@ -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,3 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { FigmaBridge } from '../ws-bridge.js';
3
+ export declare function registerStyleTools(server: McpServer, bridge: FigmaBridge): void;
@@ -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,3 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { FigmaBridge } from '../ws-bridge.js';
3
+ export declare function registerUtilityTools(server: McpServer, bridge: FigmaBridge): void;
@@ -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,3 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { FigmaBridge } from '../ws-bridge.js';
3
+ export declare function registerVariableTools(server: McpServer, bridge: FigmaBridge): void;
@@ -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
+ }