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