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