@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/dist/mcp.js
CHANGED
|
@@ -17,19 +17,14 @@ var init_serialize_node = __esm({
|
|
|
17
17
|
}
|
|
18
18
|
});
|
|
19
19
|
|
|
20
|
-
// src/tools/helpers.ts
|
|
21
|
-
var init_helpers = __esm({
|
|
22
|
-
"src/tools/helpers.ts"() {
|
|
23
|
-
init_serialize_node();
|
|
24
|
-
}
|
|
25
|
-
});
|
|
26
|
-
|
|
27
20
|
// src/mcp.ts
|
|
28
21
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
29
22
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
30
23
|
import { z as z19 } from "zod";
|
|
31
24
|
import WebSocket from "ws";
|
|
32
25
|
import { v4 as uuidv4 } from "uuid";
|
|
26
|
+
import { readFileSync } from "fs";
|
|
27
|
+
import { join, basename } from "path";
|
|
33
28
|
|
|
34
29
|
// src/tools/document.ts
|
|
35
30
|
import { z } from "zod";
|
|
@@ -183,7 +178,7 @@ function registerMcpTools2(server2, sendCommand) {
|
|
|
183
178
|
);
|
|
184
179
|
server2.tool(
|
|
185
180
|
"read_my_design",
|
|
186
|
-
"
|
|
181
|
+
"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.",
|
|
187
182
|
{ depth: z3.coerce.number().optional().describe("Levels of children to recurse. 0=selection only, -1 or omit for unlimited.") },
|
|
188
183
|
async ({ depth: depth2 }) => {
|
|
189
184
|
try {
|
|
@@ -354,8 +349,10 @@ var effectEntry = z5.object({
|
|
|
354
349
|
blendMode: z5.string().optional()
|
|
355
350
|
});
|
|
356
351
|
|
|
352
|
+
// src/tools/helpers.ts
|
|
353
|
+
init_serialize_node();
|
|
354
|
+
|
|
357
355
|
// src/tools/create-shape.ts
|
|
358
|
-
init_helpers();
|
|
359
356
|
var rectItem = z6.object({
|
|
360
357
|
name: z6.string().optional().describe("Name (default: 'Rectangle')"),
|
|
361
358
|
x: xPos,
|
|
@@ -477,7 +474,6 @@ function registerMcpTools4(server2, sendCommand) {
|
|
|
477
474
|
|
|
478
475
|
// src/tools/create-frame.ts
|
|
479
476
|
import { z as z7 } from "zod";
|
|
480
|
-
init_helpers();
|
|
481
477
|
var frameItem = z7.object({
|
|
482
478
|
name: z7.string().optional().describe("Frame name (default: 'Frame')"),
|
|
483
479
|
x: xPos,
|
|
@@ -524,7 +520,7 @@ var autoLayoutItem = z7.object({
|
|
|
524
520
|
function registerMcpTools5(server2, sendCommand) {
|
|
525
521
|
server2.tool(
|
|
526
522
|
"create_frame",
|
|
527
|
-
"Create frames in Figma.
|
|
523
|
+
"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.",
|
|
528
524
|
{ items: flexJson(z7.array(frameItem)).describe("Array of frames to create"), depth },
|
|
529
525
|
async (params) => {
|
|
530
526
|
try {
|
|
@@ -550,14 +546,15 @@ function registerMcpTools5(server2, sendCommand) {
|
|
|
550
546
|
|
|
551
547
|
// src/tools/create-text.ts
|
|
552
548
|
import { z as z8 } from "zod";
|
|
553
|
-
init_helpers();
|
|
554
549
|
var textItem = z8.object({
|
|
555
550
|
text: z8.string().describe("Text content"),
|
|
556
551
|
name: z8.string().optional().describe("Layer name (default: text content)"),
|
|
557
552
|
x: xPos,
|
|
558
553
|
y: yPos,
|
|
554
|
+
fontFamily: z8.string().optional().describe("Font family (default: Inter). Use get_available_fonts to list installed fonts."),
|
|
555
|
+
fontStyle: z8.string().optional().describe("Font style, e.g. 'Regular', 'Bold', 'Italic' (default: derived from fontWeight). Overrides fontWeight when set."),
|
|
559
556
|
fontSize: z8.coerce.number().optional().describe("Font size (default: 14)"),
|
|
560
|
-
fontWeight: z8.coerce.number().optional().describe("Font weight: 100-900 (default: 400)"),
|
|
557
|
+
fontWeight: z8.coerce.number().optional().describe("Font weight: 100-900 (default: 400). Ignored when fontStyle is set."),
|
|
561
558
|
fontColor: flexJson(colorRgba).optional().describe('Font color. Hex "#000000" or {r,g,b,a?} 0-1. Default: black.'),
|
|
562
559
|
fontColorVariableId: z8.string().optional().describe("Bind a color variable to the text fill instead of hardcoded fontColor."),
|
|
563
560
|
fontColorStyleName: z8.string().optional().describe("Apply a paint style to the text fill by name (case-insensitive). Overrides fontColor."),
|
|
@@ -573,7 +570,7 @@ var textItem = z8.object({
|
|
|
573
570
|
function registerMcpTools6(server2, sendCommand) {
|
|
574
571
|
server2.tool(
|
|
575
572
|
"create_text",
|
|
576
|
-
"Create text nodes
|
|
573
|
+
"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.",
|
|
577
574
|
{ items: flexJson(z8.array(textItem).max(10)).describe("Array of text nodes to create (max 10)"), depth },
|
|
578
575
|
async (params) => {
|
|
579
576
|
try {
|
|
@@ -587,7 +584,6 @@ function registerMcpTools6(server2, sendCommand) {
|
|
|
587
584
|
|
|
588
585
|
// src/tools/modify-node.ts
|
|
589
586
|
import { z as z9 } from "zod";
|
|
590
|
-
init_helpers();
|
|
591
587
|
var moveItem = z9.object({
|
|
592
588
|
nodeId,
|
|
593
589
|
x: z9.coerce.number().describe("New X"),
|
|
@@ -677,7 +673,6 @@ function registerMcpTools7(server2, sendCommand) {
|
|
|
677
673
|
|
|
678
674
|
// src/tools/fill-stroke.ts
|
|
679
675
|
import { z as z10 } from "zod";
|
|
680
|
-
init_helpers();
|
|
681
676
|
var fillItem = z10.object({
|
|
682
677
|
nodeId,
|
|
683
678
|
color: flexJson(colorRgba).optional().describe('Fill color. Hex "#FF0000" or {r,g,b,a?} 0-1. Ignored when styleName is set.'),
|
|
@@ -701,7 +696,7 @@ var opacityItem = z10.object({
|
|
|
701
696
|
function registerMcpTools8(server2, sendCommand) {
|
|
702
697
|
server2.tool(
|
|
703
698
|
"set_fill_color",
|
|
704
|
-
"Set fill color on nodes.
|
|
699
|
+
"Set fill color on nodes. Prefer styleName (design token) over hardcoded color \u2014 hardcoded values trigger lint warnings. Batch: pass multiple items.",
|
|
705
700
|
{ items: flexJson(z10.array(fillItem)).describe("Array of {nodeId, color?, styleName?}"), depth },
|
|
706
701
|
async (params) => {
|
|
707
702
|
try {
|
|
@@ -713,7 +708,7 @@ function registerMcpTools8(server2, sendCommand) {
|
|
|
713
708
|
);
|
|
714
709
|
server2.tool(
|
|
715
710
|
"set_stroke_color",
|
|
716
|
-
"Set stroke color on nodes.
|
|
711
|
+
"Set stroke color on nodes. Prefer styleName (design token) over hardcoded color \u2014 hardcoded values trigger lint warnings. Batch: pass multiple items.",
|
|
717
712
|
{ items: flexJson(z10.array(strokeItem)).describe("Array of {nodeId, color?, strokeWeight?, styleName?}"), depth },
|
|
718
713
|
async (params) => {
|
|
719
714
|
try {
|
|
@@ -751,7 +746,6 @@ function registerMcpTools8(server2, sendCommand) {
|
|
|
751
746
|
|
|
752
747
|
// src/tools/update-frame.ts
|
|
753
748
|
import { z as z11 } from "zod";
|
|
754
|
-
init_helpers();
|
|
755
749
|
var updateFrameItem = z11.object({
|
|
756
750
|
nodeId,
|
|
757
751
|
layoutMode: z11.enum(["NONE", "HORIZONTAL", "VERTICAL"]).optional().describe("Auto-layout direction"),
|
|
@@ -784,7 +778,6 @@ function registerMcpTools9(server2, sendCommand) {
|
|
|
784
778
|
|
|
785
779
|
// src/tools/effects.ts
|
|
786
780
|
import { z as z12 } from "zod";
|
|
787
|
-
init_helpers();
|
|
788
781
|
var effectItem = z12.object({
|
|
789
782
|
nodeId,
|
|
790
783
|
effects: flexJson(z12.array(effectEntry)).optional().describe("Array of effect objects. Ignored when effectStyleName is set."),
|
|
@@ -865,7 +858,6 @@ function registerMcpTools10(server2, sendCommand) {
|
|
|
865
858
|
|
|
866
859
|
// src/tools/text.ts
|
|
867
860
|
import { z as z13 } from "zod";
|
|
868
|
-
init_helpers();
|
|
869
861
|
var textContentItem = z13.object({
|
|
870
862
|
nodeId: z13.string().describe("Text node ID"),
|
|
871
863
|
text: z13.string().describe("New text content")
|
|
@@ -947,7 +939,6 @@ function registerMcpTools12(server2, sendCommand) {
|
|
|
947
939
|
|
|
948
940
|
// src/tools/components.ts
|
|
949
941
|
import { z as z15 } from "zod";
|
|
950
|
-
init_helpers();
|
|
951
942
|
var componentItem = z15.object({
|
|
952
943
|
name: z15.string().describe("Component name"),
|
|
953
944
|
x: xPos,
|
|
@@ -1002,7 +993,7 @@ var instanceItem = z15.object({
|
|
|
1002
993
|
function registerMcpTools13(server2, sendCommand) {
|
|
1003
994
|
server2.tool(
|
|
1004
995
|
"create_component",
|
|
1005
|
-
"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.",
|
|
996
|
+
"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.",
|
|
1006
997
|
{ items: flexJson(z15.array(componentItem)).describe("Array of components to create"), depth },
|
|
1007
998
|
async (params) => {
|
|
1008
999
|
try {
|
|
@@ -1104,11 +1095,25 @@ function registerMcpTools13(server2, sendCommand) {
|
|
|
1104
1095
|
}
|
|
1105
1096
|
}
|
|
1106
1097
|
);
|
|
1098
|
+
server2.tool(
|
|
1099
|
+
"set_instance_properties",
|
|
1100
|
+
"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.",
|
|
1101
|
+
{ items: flexJson(z15.array(z15.object({
|
|
1102
|
+
nodeId,
|
|
1103
|
+
properties: flexJson(z15.record(z15.string(), z15.union([z15.string(), z15.boolean()]))).describe('Property key\u2192value map, e.g. {"Label#1:0":"Click Me"}')
|
|
1104
|
+
}))).describe("Array of {nodeId, properties}"), depth },
|
|
1105
|
+
async (params) => {
|
|
1106
|
+
try {
|
|
1107
|
+
return mcpJson(await sendCommand("set_instance_properties", params));
|
|
1108
|
+
} catch (e) {
|
|
1109
|
+
return mcpError("Error setting instance properties", e);
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
);
|
|
1107
1113
|
}
|
|
1108
1114
|
|
|
1109
1115
|
// src/tools/styles.ts
|
|
1110
1116
|
import { z as z16 } from "zod";
|
|
1111
|
-
init_helpers();
|
|
1112
1117
|
var paintStyleItem = z16.object({
|
|
1113
1118
|
name: z16.string().describe("Style name"),
|
|
1114
1119
|
color: flexJson(colorRgba).describe('Color. Hex "#FF0000" or {r,g,b,a?} 0-1.')
|
|
@@ -1133,6 +1138,28 @@ var effectStyleItem = z16.object({
|
|
|
1133
1138
|
name: z16.string().describe("Style name"),
|
|
1134
1139
|
effects: flexJson(z16.array(effectEntry)).describe("Array of effects")
|
|
1135
1140
|
});
|
|
1141
|
+
var updatePaintStyleItem = z16.object({
|
|
1142
|
+
id: z16.string().describe("Style ID or name (case-insensitive match)"),
|
|
1143
|
+
name: z16.string().optional().describe("New name"),
|
|
1144
|
+
color: flexJson(colorRgba).optional().describe('New color. Hex "#FF0000" or {r,g,b,a?} 0-1.')
|
|
1145
|
+
});
|
|
1146
|
+
var updateTextStyleItem = z16.object({
|
|
1147
|
+
id: z16.string().describe("Style ID or name (case-insensitive match)"),
|
|
1148
|
+
name: z16.string().optional().describe("New name"),
|
|
1149
|
+
fontFamily: z16.string().optional().describe("Font family"),
|
|
1150
|
+
fontStyle: z16.string().optional().describe("Font style (e.g. Regular, Bold)"),
|
|
1151
|
+
fontSize: z16.coerce.number().optional().describe("Font size"),
|
|
1152
|
+
lineHeight: flexNum(z16.union([
|
|
1153
|
+
z16.number(),
|
|
1154
|
+
z16.object({ value: z16.coerce.number(), unit: z16.enum(["PIXELS", "PERCENT", "AUTO"]) })
|
|
1155
|
+
])).optional().describe("Line height \u2014 number (px) or {value, unit}. Default: auto."),
|
|
1156
|
+
letterSpacing: flexNum(z16.union([
|
|
1157
|
+
z16.number(),
|
|
1158
|
+
z16.object({ value: z16.coerce.number(), unit: z16.enum(["PIXELS", "PERCENT"]) })
|
|
1159
|
+
])).optional().describe("Letter spacing \u2014 number (px) or {value, unit}. Default: 0."),
|
|
1160
|
+
textCase: z16.enum(["ORIGINAL", "UPPER", "LOWER", "TITLE"]).optional(),
|
|
1161
|
+
textDecoration: z16.enum(["NONE", "UNDERLINE", "STRIKETHROUGH"]).optional()
|
|
1162
|
+
});
|
|
1136
1163
|
var applyStyleItem = z16.object({
|
|
1137
1164
|
nodeId,
|
|
1138
1165
|
styleId: z16.string().optional().describe("Style ID. Provide either styleId or styleName."),
|
|
@@ -1224,11 +1251,34 @@ function registerMcpTools14(server2, sendCommand) {
|
|
|
1224
1251
|
}
|
|
1225
1252
|
}
|
|
1226
1253
|
);
|
|
1254
|
+
server2.tool(
|
|
1255
|
+
"update_paint_style",
|
|
1256
|
+
"Update paint style color/name by ID or name. Changes propagate to all nodes using the style. Batch: pass multiple items.",
|
|
1257
|
+
{ items: flexJson(z16.array(updatePaintStyleItem)).describe("Array of {id, name?, color?}") },
|
|
1258
|
+
async (params) => {
|
|
1259
|
+
try {
|
|
1260
|
+
return mcpJson(await sendCommand("update_paint_style", params));
|
|
1261
|
+
} catch (e) {
|
|
1262
|
+
return mcpError("Error updating paint style", e);
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
);
|
|
1266
|
+
server2.tool(
|
|
1267
|
+
"update_text_style",
|
|
1268
|
+
"Update text style properties by ID or name. Changes propagate to all nodes using the style. Batch: pass multiple items.",
|
|
1269
|
+
{ items: flexJson(z16.array(updateTextStyleItem)).describe("Array of {id, name?, fontSize?, fontFamily?, ...}") },
|
|
1270
|
+
async (params) => {
|
|
1271
|
+
try {
|
|
1272
|
+
return mcpJson(await sendCommand("update_text_style", params));
|
|
1273
|
+
} catch (e) {
|
|
1274
|
+
return mcpError("Error updating text style", e);
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
);
|
|
1227
1278
|
}
|
|
1228
1279
|
|
|
1229
1280
|
// src/tools/variables.ts
|
|
1230
1281
|
import { z as z17 } from "zod";
|
|
1231
|
-
init_helpers();
|
|
1232
1282
|
var collectionItem = z17.object({
|
|
1233
1283
|
name: z17.string().describe("Collection name")
|
|
1234
1284
|
});
|
|
@@ -1242,10 +1292,9 @@ var setValueItem = z17.object({
|
|
|
1242
1292
|
modeId: z17.string().describe("Mode ID"),
|
|
1243
1293
|
value: flexJson(z17.union([
|
|
1244
1294
|
z17.number(),
|
|
1245
|
-
z17.string(),
|
|
1246
1295
|
z17.boolean(),
|
|
1247
|
-
|
|
1248
|
-
])).describe(
|
|
1296
|
+
colorRgba
|
|
1297
|
+
])).describe('Value: number, boolean, or color (hex "#RRGGBB" or {r,g,b,a?} 0-1)')
|
|
1249
1298
|
});
|
|
1250
1299
|
var bindingItem = z17.object({
|
|
1251
1300
|
nodeId: z17.string().describe("Node ID"),
|
|
@@ -1431,11 +1480,23 @@ function registerMcpTools15(server2, sendCommand) {
|
|
|
1431
1480
|
}
|
|
1432
1481
|
}
|
|
1433
1482
|
);
|
|
1483
|
+
server2.tool(
|
|
1484
|
+
"delete_variable_collection",
|
|
1485
|
+
"Delete a variable collection and all its variables. This is destructive and cannot be undone.",
|
|
1486
|
+
{ collectionId: z17.string().describe("Collection ID to delete") },
|
|
1487
|
+
async ({ collectionId }) => {
|
|
1488
|
+
try {
|
|
1489
|
+
return mcpJson(await sendCommand("delete_variable_collection", { collectionId }));
|
|
1490
|
+
} catch (e) {
|
|
1491
|
+
return mcpError("Error deleting variable collection", e);
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
);
|
|
1434
1495
|
}
|
|
1435
1496
|
|
|
1436
1497
|
// src/tools/lint.ts
|
|
1437
1498
|
import { z as z18 } from "zod";
|
|
1438
|
-
|
|
1499
|
+
init_color();
|
|
1439
1500
|
var lintRules = z18.enum([
|
|
1440
1501
|
"no-autolayout",
|
|
1441
1502
|
// Frames with >1 child and no auto-layout
|
|
@@ -1453,8 +1514,25 @@ var lintRules = z18.enum([
|
|
|
1453
1514
|
// Frames/components with layout but no children
|
|
1454
1515
|
"stale-text-name",
|
|
1455
1516
|
// Text nodes where layer name diverges from content
|
|
1517
|
+
"no-text-property",
|
|
1518
|
+
// Text in components not bound to a component property
|
|
1519
|
+
// ── WCAG 2.2 rules ──
|
|
1520
|
+
"wcag-contrast",
|
|
1521
|
+
// 1.4.3 AA text contrast (4.5:1 / 3:1 large)
|
|
1522
|
+
"wcag-contrast-enhanced",
|
|
1523
|
+
// 1.4.6 AAA text contrast (7:1 / 4.5:1 large)
|
|
1524
|
+
"wcag-non-text-contrast",
|
|
1525
|
+
// 1.4.11 AA non-text contrast (3:1)
|
|
1526
|
+
"wcag-target-size",
|
|
1527
|
+
// 2.5.8 AA target size minimum (24x24px)
|
|
1528
|
+
"wcag-text-size",
|
|
1529
|
+
// Best practice: minimum readable text (12px)
|
|
1530
|
+
"wcag-line-height",
|
|
1531
|
+
// 1.4.12 AA text spacing (line height 1.5x)
|
|
1532
|
+
"wcag",
|
|
1533
|
+
// Meta: run all wcag-* rules
|
|
1456
1534
|
"all"
|
|
1457
|
-
// Run all rules
|
|
1535
|
+
// Run all rules (including WCAG)
|
|
1458
1536
|
]);
|
|
1459
1537
|
function registerMcpTools16(server2, sendCommand) {
|
|
1460
1538
|
server2.tool(
|
|
@@ -1462,7 +1540,7 @@ function registerMcpTools16(server2, sendCommand) {
|
|
|
1462
1540
|
"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.",
|
|
1463
1541
|
{
|
|
1464
1542
|
nodeId: z18.string().optional().describe("Node ID to lint. Omit to lint current selection."),
|
|
1465
|
-
rules: flexJson(z18.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'),
|
|
1543
|
+
rules: flexJson(z18.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'),
|
|
1466
1544
|
maxDepth: z18.coerce.number().optional().describe("Max depth to recurse (default: 10)"),
|
|
1467
1545
|
maxFindings: z18.coerce.number().optional().describe("Stop after N findings (default: 50)")
|
|
1468
1546
|
},
|
|
@@ -1786,6 +1864,22 @@ function registerAllTools(server2, sendCommand) {
|
|
|
1786
1864
|
}
|
|
1787
1865
|
|
|
1788
1866
|
// src/mcp.ts
|
|
1867
|
+
var VIBMA_VERSION = "0.0.0";
|
|
1868
|
+
try {
|
|
1869
|
+
const start = typeof __dirname !== "undefined" ? __dirname : process.cwd();
|
|
1870
|
+
for (let dir = start; dir !== "/"; dir = join(dir, "..")) {
|
|
1871
|
+
try {
|
|
1872
|
+
const pkg = JSON.parse(readFileSync(join(dir, "package.json"), "utf8"));
|
|
1873
|
+
if (pkg.name === "@ufira/vibma") {
|
|
1874
|
+
VIBMA_VERSION = pkg.version;
|
|
1875
|
+
break;
|
|
1876
|
+
}
|
|
1877
|
+
} catch {
|
|
1878
|
+
continue;
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
} catch {
|
|
1882
|
+
}
|
|
1789
1883
|
var logger = {
|
|
1790
1884
|
info: (msg) => process.stderr.write(`[INFO] ${msg}
|
|
1791
1885
|
`),
|
|
@@ -1802,6 +1896,8 @@ var ws = null;
|
|
|
1802
1896
|
var pendingRequests = /* @__PURE__ */ new Map();
|
|
1803
1897
|
var currentChannel = null;
|
|
1804
1898
|
var activePort = parseInt(process.env.VIBMA_PORT || "3055");
|
|
1899
|
+
var rejected = false;
|
|
1900
|
+
var versionWarning = null;
|
|
1805
1901
|
var args = process.argv.slice(2);
|
|
1806
1902
|
var serverArg = args.find((a) => a.startsWith("--server="));
|
|
1807
1903
|
var portArg = args.find((a) => a.startsWith("--port="));
|
|
@@ -1824,6 +1920,34 @@ function connectToFigma(port = activePort) {
|
|
|
1824
1920
|
ws.on("message", (data) => {
|
|
1825
1921
|
try {
|
|
1826
1922
|
const json = JSON.parse(data);
|
|
1923
|
+
if (json.type === "join-success") {
|
|
1924
|
+
logger.info(json.message);
|
|
1925
|
+
if (json.id && pendingRequests.has(json.id)) {
|
|
1926
|
+
const req = pendingRequests.get(json.id);
|
|
1927
|
+
clearTimeout(req.timeout);
|
|
1928
|
+
req.resolve({ status: "already_joined", channel: json.channel });
|
|
1929
|
+
pendingRequests.delete(json.id);
|
|
1930
|
+
}
|
|
1931
|
+
return;
|
|
1932
|
+
}
|
|
1933
|
+
if (json.type === "system" && json.code) {
|
|
1934
|
+
if (json.code === "VERSION_MISMATCH") {
|
|
1935
|
+
versionWarning = json.message;
|
|
1936
|
+
logger.warn(`Version mismatch: ${json.message}`);
|
|
1937
|
+
}
|
|
1938
|
+
return;
|
|
1939
|
+
}
|
|
1940
|
+
if (json.type === "error") {
|
|
1941
|
+
logger.error(`Relay error: ${json.message}`);
|
|
1942
|
+
if (json.code === "ROLE_OCCUPIED") rejected = true;
|
|
1943
|
+
if (json.id && pendingRequests.has(json.id)) {
|
|
1944
|
+
const req = pendingRequests.get(json.id);
|
|
1945
|
+
clearTimeout(req.timeout);
|
|
1946
|
+
req.reject(new Error(json.message));
|
|
1947
|
+
pendingRequests.delete(json.id);
|
|
1948
|
+
}
|
|
1949
|
+
return;
|
|
1950
|
+
}
|
|
1827
1951
|
if (json.type === "progress_update") {
|
|
1828
1952
|
const progressData = json.message.data;
|
|
1829
1953
|
const requestId = json.id || "";
|
|
@@ -1875,13 +1999,23 @@ function connectToFigma(port = activePort) {
|
|
|
1875
1999
|
request.reject(new Error("Connection closed"));
|
|
1876
2000
|
pendingRequests.delete(id);
|
|
1877
2001
|
}
|
|
1878
|
-
|
|
1879
|
-
|
|
2002
|
+
if (rejected) {
|
|
2003
|
+
logger.info("Not reconnecting \u2014 channel role was rejected. Call join_channel to retry.");
|
|
2004
|
+
} else {
|
|
2005
|
+
logger.info("Attempting to reconnect in 2 seconds...");
|
|
2006
|
+
setTimeout(() => connectToFigma(port), 2e3);
|
|
2007
|
+
}
|
|
1880
2008
|
});
|
|
1881
2009
|
}
|
|
1882
2010
|
async function joinChannel(channelName) {
|
|
2011
|
+
rejected = false;
|
|
2012
|
+
versionWarning = null;
|
|
1883
2013
|
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
|
1884
|
-
|
|
2014
|
+
connectToFigma();
|
|
2015
|
+
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
2016
|
+
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
|
2017
|
+
throw new Error("Not connected to relay. Check that the relay server is running.");
|
|
2018
|
+
}
|
|
1885
2019
|
}
|
|
1886
2020
|
try {
|
|
1887
2021
|
await sendCommandToFigma("join", { channel: channelName });
|
|
@@ -1908,7 +2042,7 @@ function sendCommandToFigma(command, params = {}, timeoutMs = 3e4) {
|
|
|
1908
2042
|
const request = {
|
|
1909
2043
|
id,
|
|
1910
2044
|
type: command === "join" ? "join" : "message",
|
|
1911
|
-
...command === "join" ? { channel: params.channel } : { channel: currentChannel },
|
|
2045
|
+
...command === "join" ? { channel: params.channel, role: "mcp", version: VIBMA_VERSION, name: basename(process.cwd()) } : { channel: currentChannel },
|
|
1912
2046
|
message: {
|
|
1913
2047
|
id,
|
|
1914
2048
|
command,
|
|
@@ -1944,8 +2078,14 @@ server.tool(
|
|
|
1944
2078
|
async ({ channel }) => {
|
|
1945
2079
|
try {
|
|
1946
2080
|
await joinChannel(channel);
|
|
2081
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
2082
|
+
let msg = `Joined channel "${channel}" on port ${activePort}. Call \`ping\` now to verify the Figma plugin is connected.`;
|
|
2083
|
+
if (versionWarning) msg += `
|
|
2084
|
+
|
|
2085
|
+
\u26A0\uFE0F ${versionWarning}
|
|
2086
|
+
See "Version mismatch" in CARRYME.md or DRAGME.md for update steps.`;
|
|
1947
2087
|
return {
|
|
1948
|
-
content: [{ type: "text", text:
|
|
2088
|
+
content: [{ type: "text", text: msg }]
|
|
1949
2089
|
};
|
|
1950
2090
|
} catch (error) {
|
|
1951
2091
|
return {
|
|
@@ -1957,7 +2097,92 @@ server.tool(
|
|
|
1957
2097
|
}
|
|
1958
2098
|
}
|
|
1959
2099
|
);
|
|
2100
|
+
server.tool(
|
|
2101
|
+
"channel_info",
|
|
2102
|
+
"Debug: inspect which clients (MCP, plugin) are connected to each relay channel. Useful for diagnosing connection issues. Does not require an active channel.",
|
|
2103
|
+
{},
|
|
2104
|
+
async () => {
|
|
2105
|
+
try {
|
|
2106
|
+
const url = serverUrl === "localhost" ? `http://localhost:${activePort}/channels` : `https://${serverUrl}/channels`;
|
|
2107
|
+
const response = await fetch(url);
|
|
2108
|
+
if (!response.ok) {
|
|
2109
|
+
return { content: [{ type: "text", text: `Relay returned ${response.status}: ${await response.text()}` }] };
|
|
2110
|
+
}
|
|
2111
|
+
const data = await response.json();
|
|
2112
|
+
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
2113
|
+
} catch (error) {
|
|
2114
|
+
return {
|
|
2115
|
+
content: [{
|
|
2116
|
+
type: "text",
|
|
2117
|
+
text: `Could not reach relay at port ${activePort}: ${error instanceof Error ? error.message : String(error)}`
|
|
2118
|
+
}]
|
|
2119
|
+
};
|
|
2120
|
+
}
|
|
2121
|
+
}
|
|
2122
|
+
);
|
|
2123
|
+
server.tool(
|
|
2124
|
+
"reset_tunnel",
|
|
2125
|
+
"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`.",
|
|
2126
|
+
{
|
|
2127
|
+
channel: z19.string().describe("Channel to reset. Defaults to 'vibma'.").default("vibma")
|
|
2128
|
+
},
|
|
2129
|
+
async ({ channel }) => {
|
|
2130
|
+
const targetChannel = channel || currentChannel || "vibma";
|
|
2131
|
+
try {
|
|
2132
|
+
const url = serverUrl === "localhost" ? `http://localhost:${activePort}/channels/${encodeURIComponent(targetChannel)}` : `https://${serverUrl}/channels/${encodeURIComponent(targetChannel)}`;
|
|
2133
|
+
const res = await fetch(url, { method: "DELETE" });
|
|
2134
|
+
const body = await res.json();
|
|
2135
|
+
for (const [reqId, request] of pendingRequests.entries()) {
|
|
2136
|
+
clearTimeout(request.timeout);
|
|
2137
|
+
request.reject(new Error("Tunnel reset by user"));
|
|
2138
|
+
pendingRequests.delete(reqId);
|
|
2139
|
+
}
|
|
2140
|
+
if (ws) {
|
|
2141
|
+
const old = ws;
|
|
2142
|
+
ws = null;
|
|
2143
|
+
old.removeAllListeners();
|
|
2144
|
+
old.close(1e3, "Tunnel reset");
|
|
2145
|
+
}
|
|
2146
|
+
currentChannel = null;
|
|
2147
|
+
rejected = false;
|
|
2148
|
+
connectToFigma();
|
|
2149
|
+
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
2150
|
+
const connected = ws && ws.readyState === WebSocket.OPEN;
|
|
2151
|
+
return {
|
|
2152
|
+
content: [{
|
|
2153
|
+
type: "text",
|
|
2154
|
+
text: connected ? `Tunnel reset: ${body.message}. Reconnected on port ${activePort}.
|
|
2155
|
+
|
|
2156
|
+
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.
|
|
2157
|
+
|
|
2158
|
+
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.`
|
|
2159
|
+
}]
|
|
2160
|
+
};
|
|
2161
|
+
} catch (error) {
|
|
2162
|
+
return {
|
|
2163
|
+
content: [{
|
|
2164
|
+
type: "text",
|
|
2165
|
+
text: `Error resetting tunnel: ${error instanceof Error ? error.message : String(error)}`
|
|
2166
|
+
}]
|
|
2167
|
+
};
|
|
2168
|
+
}
|
|
2169
|
+
}
|
|
2170
|
+
);
|
|
1960
2171
|
registerAllTools(server, sendCommandToFigma);
|
|
2172
|
+
function cleanup() {
|
|
2173
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
2174
|
+
ws.close(1e3, "MCP server shutting down");
|
|
2175
|
+
}
|
|
2176
|
+
}
|
|
2177
|
+
process.on("SIGINT", () => {
|
|
2178
|
+
cleanup();
|
|
2179
|
+
process.exit(0);
|
|
2180
|
+
});
|
|
2181
|
+
process.on("SIGTERM", () => {
|
|
2182
|
+
cleanup();
|
|
2183
|
+
process.exit(0);
|
|
2184
|
+
});
|
|
2185
|
+
process.on("exit", cleanup);
|
|
1961
2186
|
async function main() {
|
|
1962
2187
|
try {
|
|
1963
2188
|
connectToFigma();
|