@ufira/vibma 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/mcp.cjs +261 -36
- package/dist/mcp.cjs.map +1 -1
- package/dist/mcp.js +261 -36
- package/dist/mcp.js.map +1 -1
- package/package.json +11 -10
package/README.md
CHANGED
|
@@ -28,13 +28,13 @@ Two paths:
|
|
|
28
28
|
| | Guide | For |
|
|
29
29
|
|---|---|---|
|
|
30
30
|
| 📦 | [**DRAGME.md**](./DRAGME.md) | Clone the repo, build from source, full control |
|
|
31
|
-
| ☁️ | [**CARRYME.md**](./CARRYME.md) | Install from
|
|
31
|
+
| ☁️ | [**CARRYME.md**](./CARRYME.md) | Install from npm, zero cloning |
|
|
32
32
|
|
|
33
33
|
Or just paste this to your AI agent and let it figure it out:
|
|
34
34
|
|
|
35
35
|
```
|
|
36
36
|
Set up Vibma so I can vibe-design in Figma.
|
|
37
|
-
Follow the instructions at https://raw.githubusercontent.com/ufira-ai/vibma/refs/heads/main/
|
|
37
|
+
Follow the instructions at https://raw.githubusercontent.com/ufira-ai/vibma/refs/heads/main/CARRYME.md
|
|
38
38
|
```
|
|
39
39
|
|
|
40
40
|
## Community
|
package/dist/mcp.cjs
CHANGED
|
@@ -38,19 +38,14 @@ var init_serialize_node = __esm({
|
|
|
38
38
|
}
|
|
39
39
|
});
|
|
40
40
|
|
|
41
|
-
// src/tools/helpers.ts
|
|
42
|
-
var init_helpers = __esm({
|
|
43
|
-
"src/tools/helpers.ts"() {
|
|
44
|
-
init_serialize_node();
|
|
45
|
-
}
|
|
46
|
-
});
|
|
47
|
-
|
|
48
41
|
// src/mcp.ts
|
|
49
42
|
var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
50
43
|
var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
51
44
|
var import_zod19 = require("zod");
|
|
52
45
|
var import_ws = __toESM(require("ws"), 1);
|
|
53
46
|
var import_uuid = require("uuid");
|
|
47
|
+
var import_fs = require("fs");
|
|
48
|
+
var import_path = require("path");
|
|
54
49
|
|
|
55
50
|
// src/tools/document.ts
|
|
56
51
|
var import_zod = require("zod");
|
|
@@ -204,7 +199,7 @@ function registerMcpTools2(server2, sendCommand) {
|
|
|
204
199
|
);
|
|
205
200
|
server2.tool(
|
|
206
201
|
"read_my_design",
|
|
207
|
-
"
|
|
202
|
+
"Read the nodes the user has selected in Figma (or set via set_selection). Returns nothing if no selection exists \u2014 ask the user to select something, or use get_node_info with specific node IDs. Use depth to control child traversal.",
|
|
208
203
|
{ depth: import_zod3.z.coerce.number().optional().describe("Levels of children to recurse. 0=selection only, -1 or omit for unlimited.") },
|
|
209
204
|
async ({ depth: depth2 }) => {
|
|
210
205
|
try {
|
|
@@ -375,8 +370,10 @@ var effectEntry = import_zod5.z.object({
|
|
|
375
370
|
blendMode: import_zod5.z.string().optional()
|
|
376
371
|
});
|
|
377
372
|
|
|
373
|
+
// src/tools/helpers.ts
|
|
374
|
+
init_serialize_node();
|
|
375
|
+
|
|
378
376
|
// src/tools/create-shape.ts
|
|
379
|
-
init_helpers();
|
|
380
377
|
var rectItem = import_zod6.z.object({
|
|
381
378
|
name: import_zod6.z.string().optional().describe("Name (default: 'Rectangle')"),
|
|
382
379
|
x: xPos,
|
|
@@ -498,7 +495,6 @@ function registerMcpTools4(server2, sendCommand) {
|
|
|
498
495
|
|
|
499
496
|
// src/tools/create-frame.ts
|
|
500
497
|
var import_zod7 = require("zod");
|
|
501
|
-
init_helpers();
|
|
502
498
|
var frameItem = import_zod7.z.object({
|
|
503
499
|
name: import_zod7.z.string().optional().describe("Frame name (default: 'Frame')"),
|
|
504
500
|
x: xPos,
|
|
@@ -545,7 +541,7 @@ var autoLayoutItem = import_zod7.z.object({
|
|
|
545
541
|
function registerMcpTools5(server2, sendCommand) {
|
|
546
542
|
server2.tool(
|
|
547
543
|
"create_frame",
|
|
548
|
-
"Create frames in Figma.
|
|
544
|
+
"Create frames in Figma. Batch supported. Use fillStyleName/fillVariableId and strokeStyleName/strokeVariableId instead of hardcoded colors \u2014 hardcoded values skip design tokens and will trigger lint warnings.",
|
|
549
545
|
{ items: flexJson(import_zod7.z.array(frameItem)).describe("Array of frames to create"), depth },
|
|
550
546
|
async (params) => {
|
|
551
547
|
try {
|
|
@@ -571,14 +567,15 @@ function registerMcpTools5(server2, sendCommand) {
|
|
|
571
567
|
|
|
572
568
|
// src/tools/create-text.ts
|
|
573
569
|
var import_zod8 = require("zod");
|
|
574
|
-
init_helpers();
|
|
575
570
|
var textItem = import_zod8.z.object({
|
|
576
571
|
text: import_zod8.z.string().describe("Text content"),
|
|
577
572
|
name: import_zod8.z.string().optional().describe("Layer name (default: text content)"),
|
|
578
573
|
x: xPos,
|
|
579
574
|
y: yPos,
|
|
575
|
+
fontFamily: import_zod8.z.string().optional().describe("Font family (default: Inter). Use get_available_fonts to list installed fonts."),
|
|
576
|
+
fontStyle: import_zod8.z.string().optional().describe("Font style, e.g. 'Regular', 'Bold', 'Italic' (default: derived from fontWeight). Overrides fontWeight when set."),
|
|
580
577
|
fontSize: import_zod8.z.coerce.number().optional().describe("Font size (default: 14)"),
|
|
581
|
-
fontWeight: import_zod8.z.coerce.number().optional().describe("Font weight: 100-900 (default: 400)"),
|
|
578
|
+
fontWeight: import_zod8.z.coerce.number().optional().describe("Font weight: 100-900 (default: 400). Ignored when fontStyle is set."),
|
|
582
579
|
fontColor: flexJson(colorRgba).optional().describe('Font color. Hex "#000000" or {r,g,b,a?} 0-1. Default: black.'),
|
|
583
580
|
fontColorVariableId: import_zod8.z.string().optional().describe("Bind a color variable to the text fill instead of hardcoded fontColor."),
|
|
584
581
|
fontColorStyleName: import_zod8.z.string().optional().describe("Apply a paint style to the text fill by name (case-insensitive). Overrides fontColor."),
|
|
@@ -594,7 +591,7 @@ var textItem = import_zod8.z.object({
|
|
|
594
591
|
function registerMcpTools6(server2, sendCommand) {
|
|
595
592
|
server2.tool(
|
|
596
593
|
"create_text",
|
|
597
|
-
"Create text nodes
|
|
594
|
+
"Create text nodes. Max 10 per batch. Prefer textStyleName for typography and fontColorStyleName or fontColorVariableId for color \u2014 hardcoded values skip design tokens. Supports custom fonts via fontFamily.",
|
|
598
595
|
{ items: flexJson(import_zod8.z.array(textItem).max(10)).describe("Array of text nodes to create (max 10)"), depth },
|
|
599
596
|
async (params) => {
|
|
600
597
|
try {
|
|
@@ -608,7 +605,6 @@ function registerMcpTools6(server2, sendCommand) {
|
|
|
608
605
|
|
|
609
606
|
// src/tools/modify-node.ts
|
|
610
607
|
var import_zod9 = require("zod");
|
|
611
|
-
init_helpers();
|
|
612
608
|
var moveItem = import_zod9.z.object({
|
|
613
609
|
nodeId,
|
|
614
610
|
x: import_zod9.z.coerce.number().describe("New X"),
|
|
@@ -698,7 +694,6 @@ function registerMcpTools7(server2, sendCommand) {
|
|
|
698
694
|
|
|
699
695
|
// src/tools/fill-stroke.ts
|
|
700
696
|
var import_zod10 = require("zod");
|
|
701
|
-
init_helpers();
|
|
702
697
|
var fillItem = import_zod10.z.object({
|
|
703
698
|
nodeId,
|
|
704
699
|
color: flexJson(colorRgba).optional().describe('Fill color. Hex "#FF0000" or {r,g,b,a?} 0-1. Ignored when styleName is set.'),
|
|
@@ -722,7 +717,7 @@ var opacityItem = import_zod10.z.object({
|
|
|
722
717
|
function registerMcpTools8(server2, sendCommand) {
|
|
723
718
|
server2.tool(
|
|
724
719
|
"set_fill_color",
|
|
725
|
-
"Set fill color on nodes.
|
|
720
|
+
"Set fill color on nodes. Prefer styleName (design token) over hardcoded color \u2014 hardcoded values trigger lint warnings. Batch: pass multiple items.",
|
|
726
721
|
{ items: flexJson(import_zod10.z.array(fillItem)).describe("Array of {nodeId, color?, styleName?}"), depth },
|
|
727
722
|
async (params) => {
|
|
728
723
|
try {
|
|
@@ -734,7 +729,7 @@ function registerMcpTools8(server2, sendCommand) {
|
|
|
734
729
|
);
|
|
735
730
|
server2.tool(
|
|
736
731
|
"set_stroke_color",
|
|
737
|
-
"Set stroke color on nodes.
|
|
732
|
+
"Set stroke color on nodes. Prefer styleName (design token) over hardcoded color \u2014 hardcoded values trigger lint warnings. Batch: pass multiple items.",
|
|
738
733
|
{ items: flexJson(import_zod10.z.array(strokeItem)).describe("Array of {nodeId, color?, strokeWeight?, styleName?}"), depth },
|
|
739
734
|
async (params) => {
|
|
740
735
|
try {
|
|
@@ -772,7 +767,6 @@ function registerMcpTools8(server2, sendCommand) {
|
|
|
772
767
|
|
|
773
768
|
// src/tools/update-frame.ts
|
|
774
769
|
var import_zod11 = require("zod");
|
|
775
|
-
init_helpers();
|
|
776
770
|
var updateFrameItem = import_zod11.z.object({
|
|
777
771
|
nodeId,
|
|
778
772
|
layoutMode: import_zod11.z.enum(["NONE", "HORIZONTAL", "VERTICAL"]).optional().describe("Auto-layout direction"),
|
|
@@ -805,7 +799,6 @@ function registerMcpTools9(server2, sendCommand) {
|
|
|
805
799
|
|
|
806
800
|
// src/tools/effects.ts
|
|
807
801
|
var import_zod12 = require("zod");
|
|
808
|
-
init_helpers();
|
|
809
802
|
var effectItem = import_zod12.z.object({
|
|
810
803
|
nodeId,
|
|
811
804
|
effects: flexJson(import_zod12.z.array(effectEntry)).optional().describe("Array of effect objects. Ignored when effectStyleName is set."),
|
|
@@ -886,7 +879,6 @@ function registerMcpTools10(server2, sendCommand) {
|
|
|
886
879
|
|
|
887
880
|
// src/tools/text.ts
|
|
888
881
|
var import_zod13 = require("zod");
|
|
889
|
-
init_helpers();
|
|
890
882
|
var textContentItem = import_zod13.z.object({
|
|
891
883
|
nodeId: import_zod13.z.string().describe("Text node ID"),
|
|
892
884
|
text: import_zod13.z.string().describe("New text content")
|
|
@@ -968,7 +960,6 @@ function registerMcpTools12(server2, sendCommand) {
|
|
|
968
960
|
|
|
969
961
|
// src/tools/components.ts
|
|
970
962
|
var import_zod15 = require("zod");
|
|
971
|
-
init_helpers();
|
|
972
963
|
var componentItem = import_zod15.z.object({
|
|
973
964
|
name: import_zod15.z.string().describe("Component name"),
|
|
974
965
|
x: xPos,
|
|
@@ -1023,7 +1014,7 @@ var instanceItem = import_zod15.z.object({
|
|
|
1023
1014
|
function registerMcpTools13(server2, sendCommand) {
|
|
1024
1015
|
server2.tool(
|
|
1025
1016
|
"create_component",
|
|
1026
|
-
"Create components in Figma. Same layout params as create_frame. Name with 'Property=Value' pattern (e.g. 'Size=Small') if you plan to combine_as_variants later. Batch: pass multiple items.",
|
|
1017
|
+
"Create components in Figma. Same layout params as create_frame. Name with 'Property=Value' pattern (e.g. 'Size=Small') if you plan to combine_as_variants later. Use fillStyleName/fillVariableId over hardcoded colors. After adding text children, use add_component_property to expose text as editable properties. Batch: pass multiple items.",
|
|
1027
1018
|
{ items: flexJson(import_zod15.z.array(componentItem)).describe("Array of components to create"), depth },
|
|
1028
1019
|
async (params) => {
|
|
1029
1020
|
try {
|
|
@@ -1125,11 +1116,25 @@ function registerMcpTools13(server2, sendCommand) {
|
|
|
1125
1116
|
}
|
|
1126
1117
|
}
|
|
1127
1118
|
);
|
|
1119
|
+
server2.tool(
|
|
1120
|
+
"set_instance_properties",
|
|
1121
|
+
"Set component property values on instances (e.g. text, boolean, instance swap). Use get_component_by_id to discover property keys. Batch: pass multiple items.",
|
|
1122
|
+
{ items: flexJson(import_zod15.z.array(import_zod15.z.object({
|
|
1123
|
+
nodeId,
|
|
1124
|
+
properties: flexJson(import_zod15.z.record(import_zod15.z.string(), import_zod15.z.union([import_zod15.z.string(), import_zod15.z.boolean()]))).describe('Property key\u2192value map, e.g. {"Label#1:0":"Click Me"}')
|
|
1125
|
+
}))).describe("Array of {nodeId, properties}"), depth },
|
|
1126
|
+
async (params) => {
|
|
1127
|
+
try {
|
|
1128
|
+
return mcpJson(await sendCommand("set_instance_properties", params));
|
|
1129
|
+
} catch (e) {
|
|
1130
|
+
return mcpError("Error setting instance properties", e);
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
);
|
|
1128
1134
|
}
|
|
1129
1135
|
|
|
1130
1136
|
// src/tools/styles.ts
|
|
1131
1137
|
var import_zod16 = require("zod");
|
|
1132
|
-
init_helpers();
|
|
1133
1138
|
var paintStyleItem = import_zod16.z.object({
|
|
1134
1139
|
name: import_zod16.z.string().describe("Style name"),
|
|
1135
1140
|
color: flexJson(colorRgba).describe('Color. Hex "#FF0000" or {r,g,b,a?} 0-1.')
|
|
@@ -1154,6 +1159,28 @@ var effectStyleItem = import_zod16.z.object({
|
|
|
1154
1159
|
name: import_zod16.z.string().describe("Style name"),
|
|
1155
1160
|
effects: flexJson(import_zod16.z.array(effectEntry)).describe("Array of effects")
|
|
1156
1161
|
});
|
|
1162
|
+
var updatePaintStyleItem = import_zod16.z.object({
|
|
1163
|
+
id: import_zod16.z.string().describe("Style ID or name (case-insensitive match)"),
|
|
1164
|
+
name: import_zod16.z.string().optional().describe("New name"),
|
|
1165
|
+
color: flexJson(colorRgba).optional().describe('New color. Hex "#FF0000" or {r,g,b,a?} 0-1.')
|
|
1166
|
+
});
|
|
1167
|
+
var updateTextStyleItem = import_zod16.z.object({
|
|
1168
|
+
id: import_zod16.z.string().describe("Style ID or name (case-insensitive match)"),
|
|
1169
|
+
name: import_zod16.z.string().optional().describe("New name"),
|
|
1170
|
+
fontFamily: import_zod16.z.string().optional().describe("Font family"),
|
|
1171
|
+
fontStyle: import_zod16.z.string().optional().describe("Font style (e.g. Regular, Bold)"),
|
|
1172
|
+
fontSize: import_zod16.z.coerce.number().optional().describe("Font size"),
|
|
1173
|
+
lineHeight: flexNum(import_zod16.z.union([
|
|
1174
|
+
import_zod16.z.number(),
|
|
1175
|
+
import_zod16.z.object({ value: import_zod16.z.coerce.number(), unit: import_zod16.z.enum(["PIXELS", "PERCENT", "AUTO"]) })
|
|
1176
|
+
])).optional().describe("Line height \u2014 number (px) or {value, unit}. Default: auto."),
|
|
1177
|
+
letterSpacing: flexNum(import_zod16.z.union([
|
|
1178
|
+
import_zod16.z.number(),
|
|
1179
|
+
import_zod16.z.object({ value: import_zod16.z.coerce.number(), unit: import_zod16.z.enum(["PIXELS", "PERCENT"]) })
|
|
1180
|
+
])).optional().describe("Letter spacing \u2014 number (px) or {value, unit}. Default: 0."),
|
|
1181
|
+
textCase: import_zod16.z.enum(["ORIGINAL", "UPPER", "LOWER", "TITLE"]).optional(),
|
|
1182
|
+
textDecoration: import_zod16.z.enum(["NONE", "UNDERLINE", "STRIKETHROUGH"]).optional()
|
|
1183
|
+
});
|
|
1157
1184
|
var applyStyleItem = import_zod16.z.object({
|
|
1158
1185
|
nodeId,
|
|
1159
1186
|
styleId: import_zod16.z.string().optional().describe("Style ID. Provide either styleId or styleName."),
|
|
@@ -1245,11 +1272,34 @@ function registerMcpTools14(server2, sendCommand) {
|
|
|
1245
1272
|
}
|
|
1246
1273
|
}
|
|
1247
1274
|
);
|
|
1275
|
+
server2.tool(
|
|
1276
|
+
"update_paint_style",
|
|
1277
|
+
"Update paint style color/name by ID or name. Changes propagate to all nodes using the style. Batch: pass multiple items.",
|
|
1278
|
+
{ items: flexJson(import_zod16.z.array(updatePaintStyleItem)).describe("Array of {id, name?, color?}") },
|
|
1279
|
+
async (params) => {
|
|
1280
|
+
try {
|
|
1281
|
+
return mcpJson(await sendCommand("update_paint_style", params));
|
|
1282
|
+
} catch (e) {
|
|
1283
|
+
return mcpError("Error updating paint style", e);
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
);
|
|
1287
|
+
server2.tool(
|
|
1288
|
+
"update_text_style",
|
|
1289
|
+
"Update text style properties by ID or name. Changes propagate to all nodes using the style. Batch: pass multiple items.",
|
|
1290
|
+
{ items: flexJson(import_zod16.z.array(updateTextStyleItem)).describe("Array of {id, name?, fontSize?, fontFamily?, ...}") },
|
|
1291
|
+
async (params) => {
|
|
1292
|
+
try {
|
|
1293
|
+
return mcpJson(await sendCommand("update_text_style", params));
|
|
1294
|
+
} catch (e) {
|
|
1295
|
+
return mcpError("Error updating text style", e);
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
);
|
|
1248
1299
|
}
|
|
1249
1300
|
|
|
1250
1301
|
// src/tools/variables.ts
|
|
1251
1302
|
var import_zod17 = require("zod");
|
|
1252
|
-
init_helpers();
|
|
1253
1303
|
var collectionItem = import_zod17.z.object({
|
|
1254
1304
|
name: import_zod17.z.string().describe("Collection name")
|
|
1255
1305
|
});
|
|
@@ -1263,10 +1313,9 @@ var setValueItem = import_zod17.z.object({
|
|
|
1263
1313
|
modeId: import_zod17.z.string().describe("Mode ID"),
|
|
1264
1314
|
value: flexJson(import_zod17.z.union([
|
|
1265
1315
|
import_zod17.z.number(),
|
|
1266
|
-
import_zod17.z.string(),
|
|
1267
1316
|
import_zod17.z.boolean(),
|
|
1268
|
-
|
|
1269
|
-
])).describe(
|
|
1317
|
+
colorRgba
|
|
1318
|
+
])).describe('Value: number, boolean, or color (hex "#RRGGBB" or {r,g,b,a?} 0-1)')
|
|
1270
1319
|
});
|
|
1271
1320
|
var bindingItem = import_zod17.z.object({
|
|
1272
1321
|
nodeId: import_zod17.z.string().describe("Node ID"),
|
|
@@ -1452,11 +1501,23 @@ function registerMcpTools15(server2, sendCommand) {
|
|
|
1452
1501
|
}
|
|
1453
1502
|
}
|
|
1454
1503
|
);
|
|
1504
|
+
server2.tool(
|
|
1505
|
+
"delete_variable_collection",
|
|
1506
|
+
"Delete a variable collection and all its variables. This is destructive and cannot be undone.",
|
|
1507
|
+
{ collectionId: import_zod17.z.string().describe("Collection ID to delete") },
|
|
1508
|
+
async ({ collectionId }) => {
|
|
1509
|
+
try {
|
|
1510
|
+
return mcpJson(await sendCommand("delete_variable_collection", { collectionId }));
|
|
1511
|
+
} catch (e) {
|
|
1512
|
+
return mcpError("Error deleting variable collection", e);
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
);
|
|
1455
1516
|
}
|
|
1456
1517
|
|
|
1457
1518
|
// src/tools/lint.ts
|
|
1458
1519
|
var import_zod18 = require("zod");
|
|
1459
|
-
|
|
1520
|
+
init_color();
|
|
1460
1521
|
var lintRules = import_zod18.z.enum([
|
|
1461
1522
|
"no-autolayout",
|
|
1462
1523
|
// Frames with >1 child and no auto-layout
|
|
@@ -1474,8 +1535,25 @@ var lintRules = import_zod18.z.enum([
|
|
|
1474
1535
|
// Frames/components with layout but no children
|
|
1475
1536
|
"stale-text-name",
|
|
1476
1537
|
// Text nodes where layer name diverges from content
|
|
1538
|
+
"no-text-property",
|
|
1539
|
+
// Text in components not bound to a component property
|
|
1540
|
+
// ── WCAG 2.2 rules ──
|
|
1541
|
+
"wcag-contrast",
|
|
1542
|
+
// 1.4.3 AA text contrast (4.5:1 / 3:1 large)
|
|
1543
|
+
"wcag-contrast-enhanced",
|
|
1544
|
+
// 1.4.6 AAA text contrast (7:1 / 4.5:1 large)
|
|
1545
|
+
"wcag-non-text-contrast",
|
|
1546
|
+
// 1.4.11 AA non-text contrast (3:1)
|
|
1547
|
+
"wcag-target-size",
|
|
1548
|
+
// 2.5.8 AA target size minimum (24x24px)
|
|
1549
|
+
"wcag-text-size",
|
|
1550
|
+
// Best practice: minimum readable text (12px)
|
|
1551
|
+
"wcag-line-height",
|
|
1552
|
+
// 1.4.12 AA text spacing (line height 1.5x)
|
|
1553
|
+
"wcag",
|
|
1554
|
+
// Meta: run all wcag-* rules
|
|
1477
1555
|
"all"
|
|
1478
|
-
// Run all rules
|
|
1556
|
+
// Run all rules (including WCAG)
|
|
1479
1557
|
]);
|
|
1480
1558
|
function registerMcpTools16(server2, sendCommand) {
|
|
1481
1559
|
server2.tool(
|
|
@@ -1483,7 +1561,7 @@ function registerMcpTools16(server2, sendCommand) {
|
|
|
1483
1561
|
"Run design linter on a node tree. Returns issues grouped by category with affected node IDs and fix instructions. Lint child nodes individually for large trees.",
|
|
1484
1562
|
{
|
|
1485
1563
|
nodeId: import_zod18.z.string().optional().describe("Node ID to lint. Omit to lint current selection."),
|
|
1486
|
-
rules: flexJson(import_zod18.z.array(lintRules)).optional().describe('Rules to run. Default: ["all"]. Options: no-autolayout, shape-instead-of-frame, hardcoded-color, no-text-style, fixed-in-autolayout, default-name, empty-container, stale-text-name, all'),
|
|
1564
|
+
rules: flexJson(import_zod18.z.array(lintRules)).optional().describe('Rules to run. Default: ["all"]. Options: no-autolayout, shape-instead-of-frame, hardcoded-color, no-text-style, fixed-in-autolayout, default-name, empty-container, stale-text-name, no-text-property, all, wcag-contrast, wcag-contrast-enhanced, wcag-non-text-contrast, wcag-target-size, wcag-text-size, wcag-line-height, wcag'),
|
|
1487
1565
|
maxDepth: import_zod18.z.coerce.number().optional().describe("Max depth to recurse (default: 10)"),
|
|
1488
1566
|
maxFindings: import_zod18.z.coerce.number().optional().describe("Stop after N findings (default: 50)")
|
|
1489
1567
|
},
|
|
@@ -1807,6 +1885,22 @@ function registerAllTools(server2, sendCommand) {
|
|
|
1807
1885
|
}
|
|
1808
1886
|
|
|
1809
1887
|
// src/mcp.ts
|
|
1888
|
+
var VIBMA_VERSION = "0.0.0";
|
|
1889
|
+
try {
|
|
1890
|
+
const start = typeof __dirname !== "undefined" ? __dirname : process.cwd();
|
|
1891
|
+
for (let dir = start; dir !== "/"; dir = (0, import_path.join)(dir, "..")) {
|
|
1892
|
+
try {
|
|
1893
|
+
const pkg = JSON.parse((0, import_fs.readFileSync)((0, import_path.join)(dir, "package.json"), "utf8"));
|
|
1894
|
+
if (pkg.name === "@ufira/vibma") {
|
|
1895
|
+
VIBMA_VERSION = pkg.version;
|
|
1896
|
+
break;
|
|
1897
|
+
}
|
|
1898
|
+
} catch {
|
|
1899
|
+
continue;
|
|
1900
|
+
}
|
|
1901
|
+
}
|
|
1902
|
+
} catch {
|
|
1903
|
+
}
|
|
1810
1904
|
var logger = {
|
|
1811
1905
|
info: (msg) => process.stderr.write(`[INFO] ${msg}
|
|
1812
1906
|
`),
|
|
@@ -1823,6 +1917,8 @@ var ws = null;
|
|
|
1823
1917
|
var pendingRequests = /* @__PURE__ */ new Map();
|
|
1824
1918
|
var currentChannel = null;
|
|
1825
1919
|
var activePort = parseInt(process.env.VIBMA_PORT || "3055");
|
|
1920
|
+
var rejected = false;
|
|
1921
|
+
var versionWarning = null;
|
|
1826
1922
|
var args = process.argv.slice(2);
|
|
1827
1923
|
var serverArg = args.find((a) => a.startsWith("--server="));
|
|
1828
1924
|
var portArg = args.find((a) => a.startsWith("--port="));
|
|
@@ -1845,6 +1941,34 @@ function connectToFigma(port = activePort) {
|
|
|
1845
1941
|
ws.on("message", (data) => {
|
|
1846
1942
|
try {
|
|
1847
1943
|
const json = JSON.parse(data);
|
|
1944
|
+
if (json.type === "join-success") {
|
|
1945
|
+
logger.info(json.message);
|
|
1946
|
+
if (json.id && pendingRequests.has(json.id)) {
|
|
1947
|
+
const req = pendingRequests.get(json.id);
|
|
1948
|
+
clearTimeout(req.timeout);
|
|
1949
|
+
req.resolve({ status: "already_joined", channel: json.channel });
|
|
1950
|
+
pendingRequests.delete(json.id);
|
|
1951
|
+
}
|
|
1952
|
+
return;
|
|
1953
|
+
}
|
|
1954
|
+
if (json.type === "system" && json.code) {
|
|
1955
|
+
if (json.code === "VERSION_MISMATCH") {
|
|
1956
|
+
versionWarning = json.message;
|
|
1957
|
+
logger.warn(`Version mismatch: ${json.message}`);
|
|
1958
|
+
}
|
|
1959
|
+
return;
|
|
1960
|
+
}
|
|
1961
|
+
if (json.type === "error") {
|
|
1962
|
+
logger.error(`Relay error: ${json.message}`);
|
|
1963
|
+
if (json.code === "ROLE_OCCUPIED") rejected = true;
|
|
1964
|
+
if (json.id && pendingRequests.has(json.id)) {
|
|
1965
|
+
const req = pendingRequests.get(json.id);
|
|
1966
|
+
clearTimeout(req.timeout);
|
|
1967
|
+
req.reject(new Error(json.message));
|
|
1968
|
+
pendingRequests.delete(json.id);
|
|
1969
|
+
}
|
|
1970
|
+
return;
|
|
1971
|
+
}
|
|
1848
1972
|
if (json.type === "progress_update") {
|
|
1849
1973
|
const progressData = json.message.data;
|
|
1850
1974
|
const requestId = json.id || "";
|
|
@@ -1896,13 +2020,23 @@ function connectToFigma(port = activePort) {
|
|
|
1896
2020
|
request.reject(new Error("Connection closed"));
|
|
1897
2021
|
pendingRequests.delete(id);
|
|
1898
2022
|
}
|
|
1899
|
-
|
|
1900
|
-
|
|
2023
|
+
if (rejected) {
|
|
2024
|
+
logger.info("Not reconnecting \u2014 channel role was rejected. Call join_channel to retry.");
|
|
2025
|
+
} else {
|
|
2026
|
+
logger.info("Attempting to reconnect in 2 seconds...");
|
|
2027
|
+
setTimeout(() => connectToFigma(port), 2e3);
|
|
2028
|
+
}
|
|
1901
2029
|
});
|
|
1902
2030
|
}
|
|
1903
2031
|
async function joinChannel(channelName) {
|
|
2032
|
+
rejected = false;
|
|
2033
|
+
versionWarning = null;
|
|
1904
2034
|
if (!ws || ws.readyState !== import_ws.default.OPEN) {
|
|
1905
|
-
|
|
2035
|
+
connectToFigma();
|
|
2036
|
+
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
2037
|
+
if (!ws || ws.readyState !== import_ws.default.OPEN) {
|
|
2038
|
+
throw new Error("Not connected to relay. Check that the relay server is running.");
|
|
2039
|
+
}
|
|
1906
2040
|
}
|
|
1907
2041
|
try {
|
|
1908
2042
|
await sendCommandToFigma("join", { channel: channelName });
|
|
@@ -1929,7 +2063,7 @@ function sendCommandToFigma(command, params = {}, timeoutMs = 3e4) {
|
|
|
1929
2063
|
const request = {
|
|
1930
2064
|
id,
|
|
1931
2065
|
type: command === "join" ? "join" : "message",
|
|
1932
|
-
...command === "join" ? { channel: params.channel } : { channel: currentChannel },
|
|
2066
|
+
...command === "join" ? { channel: params.channel, role: "mcp", version: VIBMA_VERSION, name: (0, import_path.basename)(process.cwd()) } : { channel: currentChannel },
|
|
1933
2067
|
message: {
|
|
1934
2068
|
id,
|
|
1935
2069
|
command,
|
|
@@ -1965,8 +2099,14 @@ server.tool(
|
|
|
1965
2099
|
async ({ channel }) => {
|
|
1966
2100
|
try {
|
|
1967
2101
|
await joinChannel(channel);
|
|
2102
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
2103
|
+
let msg = `Joined channel "${channel}" on port ${activePort}. Call \`ping\` now to verify the Figma plugin is connected.`;
|
|
2104
|
+
if (versionWarning) msg += `
|
|
2105
|
+
|
|
2106
|
+
\u26A0\uFE0F ${versionWarning}
|
|
2107
|
+
See "Version mismatch" in CARRYME.md or DRAGME.md for update steps.`;
|
|
1968
2108
|
return {
|
|
1969
|
-
content: [{ type: "text", text:
|
|
2109
|
+
content: [{ type: "text", text: msg }]
|
|
1970
2110
|
};
|
|
1971
2111
|
} catch (error) {
|
|
1972
2112
|
return {
|
|
@@ -1978,7 +2118,92 @@ server.tool(
|
|
|
1978
2118
|
}
|
|
1979
2119
|
}
|
|
1980
2120
|
);
|
|
2121
|
+
server.tool(
|
|
2122
|
+
"channel_info",
|
|
2123
|
+
"Debug: inspect which clients (MCP, plugin) are connected to each relay channel. Useful for diagnosing connection issues. Does not require an active channel.",
|
|
2124
|
+
{},
|
|
2125
|
+
async () => {
|
|
2126
|
+
try {
|
|
2127
|
+
const url = serverUrl === "localhost" ? `http://localhost:${activePort}/channels` : `https://${serverUrl}/channels`;
|
|
2128
|
+
const response = await fetch(url);
|
|
2129
|
+
if (!response.ok) {
|
|
2130
|
+
return { content: [{ type: "text", text: `Relay returned ${response.status}: ${await response.text()}` }] };
|
|
2131
|
+
}
|
|
2132
|
+
const data = await response.json();
|
|
2133
|
+
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
2134
|
+
} catch (error) {
|
|
2135
|
+
return {
|
|
2136
|
+
content: [{
|
|
2137
|
+
type: "text",
|
|
2138
|
+
text: `Could not reach relay at port ${activePort}: ${error instanceof Error ? error.message : String(error)}`
|
|
2139
|
+
}]
|
|
2140
|
+
};
|
|
2141
|
+
}
|
|
2142
|
+
}
|
|
2143
|
+
);
|
|
2144
|
+
server.tool(
|
|
2145
|
+
"reset_tunnel",
|
|
2146
|
+
"DESTRUCTIVE: Factory-reset a channel on the relay via HTTP, disconnecting ALL occupants (MCP + Figma plugin). Only use this when the channel is stuck or occupied by another MCP \u2014 do NOT use it just to reconnect yourself (use `join_channel` instead). After reset, ask the user to reopen the Vibma plugin in Figma, then call `join_channel` and `ping`.",
|
|
2147
|
+
{
|
|
2148
|
+
channel: import_zod19.z.string().describe("Channel to reset. Defaults to 'vibma'.").default("vibma")
|
|
2149
|
+
},
|
|
2150
|
+
async ({ channel }) => {
|
|
2151
|
+
const targetChannel = channel || currentChannel || "vibma";
|
|
2152
|
+
try {
|
|
2153
|
+
const url = serverUrl === "localhost" ? `http://localhost:${activePort}/channels/${encodeURIComponent(targetChannel)}` : `https://${serverUrl}/channels/${encodeURIComponent(targetChannel)}`;
|
|
2154
|
+
const res = await fetch(url, { method: "DELETE" });
|
|
2155
|
+
const body = await res.json();
|
|
2156
|
+
for (const [reqId, request] of pendingRequests.entries()) {
|
|
2157
|
+
clearTimeout(request.timeout);
|
|
2158
|
+
request.reject(new Error("Tunnel reset by user"));
|
|
2159
|
+
pendingRequests.delete(reqId);
|
|
2160
|
+
}
|
|
2161
|
+
if (ws) {
|
|
2162
|
+
const old = ws;
|
|
2163
|
+
ws = null;
|
|
2164
|
+
old.removeAllListeners();
|
|
2165
|
+
old.close(1e3, "Tunnel reset");
|
|
2166
|
+
}
|
|
2167
|
+
currentChannel = null;
|
|
2168
|
+
rejected = false;
|
|
2169
|
+
connectToFigma();
|
|
2170
|
+
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
2171
|
+
const connected = ws && ws.readyState === import_ws.default.OPEN;
|
|
2172
|
+
return {
|
|
2173
|
+
content: [{
|
|
2174
|
+
type: "text",
|
|
2175
|
+
text: connected ? `Tunnel reset: ${body.message}. Reconnected on port ${activePort}.
|
|
2176
|
+
|
|
2177
|
+
IMPORTANT: The Figma plugin was also disconnected. Ask the user to reopen the Vibma plugin in Figma (or click "Reset tunnel" in the plugin panel). Then call \`join_channel\` followed by \`ping\`.` : `Tunnel reset: ${body.message}. Reconnection in progress.
|
|
2178
|
+
|
|
2179
|
+
IMPORTANT: The Figma plugin was also disconnected. Ask the user to reopen the Vibma plugin in Figma (or click "Reset tunnel" in the plugin panel). Then call \`join_channel\` to retry.`
|
|
2180
|
+
}]
|
|
2181
|
+
};
|
|
2182
|
+
} catch (error) {
|
|
2183
|
+
return {
|
|
2184
|
+
content: [{
|
|
2185
|
+
type: "text",
|
|
2186
|
+
text: `Error resetting tunnel: ${error instanceof Error ? error.message : String(error)}`
|
|
2187
|
+
}]
|
|
2188
|
+
};
|
|
2189
|
+
}
|
|
2190
|
+
}
|
|
2191
|
+
);
|
|
1981
2192
|
registerAllTools(server, sendCommandToFigma);
|
|
2193
|
+
function cleanup() {
|
|
2194
|
+
if (ws && ws.readyState === import_ws.default.OPEN) {
|
|
2195
|
+
ws.close(1e3, "MCP server shutting down");
|
|
2196
|
+
}
|
|
2197
|
+
}
|
|
2198
|
+
process.on("SIGINT", () => {
|
|
2199
|
+
cleanup();
|
|
2200
|
+
process.exit(0);
|
|
2201
|
+
});
|
|
2202
|
+
process.on("SIGTERM", () => {
|
|
2203
|
+
cleanup();
|
|
2204
|
+
process.exit(0);
|
|
2205
|
+
});
|
|
2206
|
+
process.on("exit", cleanup);
|
|
1982
2207
|
async function main() {
|
|
1983
2208
|
try {
|
|
1984
2209
|
connectToFigma();
|