ingresseflow-bridge 1.0.2 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -197,6 +197,113 @@ server.tool('figjam_create_section', 'Cria uma seção nomeada no FigJam para ag
197
197
  const result = await sendToPlugin('CREATE_SECTION', { name, x, y, width, height });
198
198
  return { content: [{ type: 'text', text: JSON.stringify(result) }] };
199
199
  });
200
+ // ── Shapes & connectors (BPMN-friendly) ───────────────────────────────────────
201
+ const ShapeTypeEnum = z.enum([
202
+ 'SQUARE', 'ELLIPSE', 'ROUNDED_RECTANGLE', 'DIAMOND',
203
+ 'TRIANGLE_UP', 'TRIANGLE_DOWN',
204
+ 'PARALLELOGRAM_RIGHT', 'PARALLELOGRAM_LEFT',
205
+ 'ENG_DATABASE', 'ENG_QUEUE', 'ENG_FILE', 'ENG_FOLDER',
206
+ 'TRAPEZOID', 'PREDEFINED_PROCESS', 'SHIELD',
207
+ 'DOCUMENT_SINGLE', 'DOCUMENT_MULTIPLE', 'MANUAL_INPUT',
208
+ 'HEXAGON', 'CHEVRON_RIGHT_ARROW', 'CHEVRON_RIGHT_PENTAGON',
209
+ ]).describe('Tipo de shape (BPMN: ELLIPSE=evento, ROUNDED_RECTANGLE=tarefa, DIAMOND=gateway)');
210
+ const MagnetEnum = z.enum(['AUTO', 'TOP', 'LEFT', 'BOTTOM', 'RIGHT', 'CENTER'])
211
+ .describe('Ponto de ancoragem do conector no nó');
212
+ const StrokeCapEnum = z.enum([
213
+ 'NONE', 'ARROW_LINES', 'ARROW_EQUILATERAL',
214
+ 'TRIANGLE_FILLED', 'DIAMOND_FILLED', 'CIRCLE_FILLED',
215
+ ]).describe('Estilo da ponta do conector');
216
+ const LineTypeEnum = z.enum(['STRAIGHT', 'ELBOWED'])
217
+ .describe('Reta direta ou com cotovelos (90°)');
218
+ const ShapeItem = z.object({
219
+ shape: ShapeTypeEnum.default('ROUNDED_RECTANGLE'),
220
+ text: z.string().optional().describe('Texto interno do shape'),
221
+ color: ColorEnum.default('BLUE'),
222
+ x: z.number().optional(),
223
+ y: z.number().optional(),
224
+ width: z.number().optional(),
225
+ height: z.number().optional(),
226
+ });
227
+ const ConnectorItem = z.object({
228
+ startNodeId: z.string().describe('ID do nó de origem'),
229
+ endNodeId: z.string().describe('ID do nó de destino'),
230
+ startMagnet: MagnetEnum.default('AUTO'),
231
+ endMagnet: MagnetEnum.default('AUTO'),
232
+ label: z.string().optional().describe('Texto sobre o conector (ex.: Sim/Não)'),
233
+ lineType: LineTypeEnum.optional(),
234
+ startCap: StrokeCapEnum.optional(),
235
+ endCap: StrokeCapEnum.optional(),
236
+ strokeWeight: z.number().optional(),
237
+ color: ColorEnum.optional(),
238
+ });
239
+ server.tool('figjam_create_shape', 'Cria uma forma com texto no FigJam (BPMN: ELLIPSE=evento, ROUNDED_RECTANGLE=tarefa, DIAMOND=gateway). Retorna nodeId para conectar.', ShapeItem.shape, async (params) => {
240
+ const result = await sendToPlugin('CREATE_SHAPE', params);
241
+ return { content: [{ type: 'text', text: JSON.stringify(result) }] };
242
+ });
243
+ server.tool('figjam_create_shapes', 'Cria múltiplas formas com texto de uma vez. Máximo 50 por chamada. Use com figjam_create_connectors para montar fluxos BPMN.', {
244
+ shapes: z.array(ShapeItem).max(50).describe('Lista de formas para criar'),
245
+ }, async ({ shapes }) => {
246
+ const result = await sendToPlugin('CREATE_SHAPES', { shapes });
247
+ return { content: [{ type: 'text', text: JSON.stringify(result) }] };
248
+ });
249
+ server.tool('figjam_create_connector', 'Conecta dois nós existentes com uma seta (BPMN sequence flow). Aceita nodeId de stickies, shapes ou qualquer SceneNode.', ConnectorItem.shape, async (params) => {
250
+ const result = await sendToPlugin('CREATE_CONNECTOR', params);
251
+ return { content: [{ type: 'text', text: JSON.stringify(result) }] };
252
+ });
253
+ server.tool('figjam_create_connectors', 'Cria múltiplos conectores de uma vez. Máximo 100 por chamada. Use após figjam_create_shapes para montar fluxos completos.', {
254
+ connectors: z.array(ConnectorItem).max(100).describe('Lista de conectores'),
255
+ }, async ({ connectors }) => {
256
+ const result = await sendToPlugin('CREATE_CONNECTORS', { connectors });
257
+ return { content: [{ type: 'text', text: JSON.stringify(result) }] };
258
+ });
259
+ server.tool('figjam_create_flow', 'Atalho: cria um fluxo linear (shapes em sequência horizontal já conectados). Para fluxos com ramificações use figjam_create_shapes + figjam_create_connectors.', {
260
+ steps: z.array(z.object({
261
+ label: z.string(),
262
+ shape: ShapeTypeEnum.optional(),
263
+ color: ColorEnum.optional(),
264
+ })).describe('Etapas do fluxo, da esquerda para a direita'),
265
+ x: z.number().optional().describe('X inicial do primeiro shape'),
266
+ y: z.number().optional().describe('Y comum a todos os shapes'),
267
+ gap: z.number().default(220).describe('Distância horizontal entre shapes'),
268
+ }, async ({ steps, x, y, gap }) => {
269
+ const result = await sendToPlugin('CREATE_FLOW', { steps, x, y, gap });
270
+ return { content: [{ type: 'text', text: JSON.stringify(result) }] };
271
+ });
272
+ server.tool('figjam_set_connector_labels', 'Define ou atualiza rótulos em conectores existentes (ex.: Sim/Não em gateways BPMN).', {
273
+ labels: z.array(z.object({
274
+ nodeId: z.string(),
275
+ label: z.string(),
276
+ })).describe('Pares { nodeId, label } para conectores existentes'),
277
+ }, async ({ labels }) => {
278
+ const result = await sendToPlugin('SET_CONNECTOR_LABELS', { labels });
279
+ return { content: [{ type: 'text', text: JSON.stringify(result) }] };
280
+ });
281
+ server.tool('figjam_move_nodes', 'Reposiciona nós existentes pelo ID.', {
282
+ moves: z.array(z.object({
283
+ nodeId: z.string(),
284
+ x: z.number(),
285
+ y: z.number(),
286
+ })),
287
+ }, async ({ moves }) => {
288
+ const result = await sendToPlugin('MOVE_NODES', { moves });
289
+ return { content: [{ type: 'text', text: JSON.stringify(result) }] };
290
+ });
291
+ server.tool('figjam_delete_nodes', 'Remove nós do canvas pelo ID.', {
292
+ nodeIds: z.array(z.string()),
293
+ }, async ({ nodeIds }) => {
294
+ const result = await sendToPlugin('DELETE_NODES', { nodeIds });
295
+ return { content: [{ type: 'text', text: JSON.stringify(result) }] };
296
+ });
297
+ // Generic forwarder — lets new plugin operations be invoked without bridge updates.
298
+ // Useful for evolving the plugin independently: any new op type added in code.ts
299
+ // becomes immediately accessible via this tool.
300
+ server.tool('figjam_execute', 'Encaminha uma operação arbitrária para o plugin (escape hatch para ops novas que ainda não têm tool dedicada). Tipos válidos atuais: PING, CREATE_STICKY, CREATE_STICKIES, CREATE_SECTION, CREATE_SHAPE, CREATE_SHAPES, CREATE_CONNECTOR, CREATE_CONNECTORS, CREATE_FLOW, SET_CONNECTOR_LABELS, MOVE_NODES, DELETE_NODES, READ_BOARD, GET_SELECTION.', {
301
+ type: z.string().describe('Nome da operação no plugin (ex.: CREATE_SHAPE)'),
302
+ payload: z.record(z.unknown()).optional().describe('Payload da operação'),
303
+ }, async ({ type, payload }) => {
304
+ const result = await sendToPlugin(type, payload ?? {});
305
+ return { content: [{ type: 'text', text: JSON.stringify(result) }] };
306
+ });
200
307
  // ── Boot ──────────────────────────────────────────────────────────────────────
201
308
  const existing = await detectExistingBridge();
202
309
  if (existing) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ingresseflow-bridge",
3
- "version": "1.0.2",
3
+ "version": "1.1.0",
4
4
  "description": "Bridge MCP server for IngresseFlow — connects Claude Code to FigJam via Figma Desktop plugin",
5
5
  "type": "module",
6
6
  "bin": {
package/src/index.ts CHANGED
@@ -250,6 +250,174 @@ server.tool(
250
250
  }
251
251
  );
252
252
 
253
+ // ── Shapes & connectors (BPMN-friendly) ───────────────────────────────────────
254
+
255
+ const ShapeTypeEnum = z.enum([
256
+ 'SQUARE', 'ELLIPSE', 'ROUNDED_RECTANGLE', 'DIAMOND',
257
+ 'TRIANGLE_UP', 'TRIANGLE_DOWN',
258
+ 'PARALLELOGRAM_RIGHT', 'PARALLELOGRAM_LEFT',
259
+ 'ENG_DATABASE', 'ENG_QUEUE', 'ENG_FILE', 'ENG_FOLDER',
260
+ 'TRAPEZOID', 'PREDEFINED_PROCESS', 'SHIELD',
261
+ 'DOCUMENT_SINGLE', 'DOCUMENT_MULTIPLE', 'MANUAL_INPUT',
262
+ 'HEXAGON', 'CHEVRON_RIGHT_ARROW', 'CHEVRON_RIGHT_PENTAGON',
263
+ ]).describe('Tipo de shape (BPMN: ELLIPSE=evento, ROUNDED_RECTANGLE=tarefa, DIAMOND=gateway)');
264
+
265
+ const MagnetEnum = z.enum(['AUTO', 'TOP', 'LEFT', 'BOTTOM', 'RIGHT', 'CENTER'])
266
+ .describe('Ponto de ancoragem do conector no nó');
267
+
268
+ const StrokeCapEnum = z.enum([
269
+ 'NONE', 'ARROW_LINES', 'ARROW_EQUILATERAL',
270
+ 'TRIANGLE_FILLED', 'DIAMOND_FILLED', 'CIRCLE_FILLED',
271
+ ]).describe('Estilo da ponta do conector');
272
+
273
+ const LineTypeEnum = z.enum(['STRAIGHT', 'ELBOWED'])
274
+ .describe('Reta direta ou com cotovelos (90°)');
275
+
276
+ const ShapeItem = z.object({
277
+ shape: ShapeTypeEnum.default('ROUNDED_RECTANGLE'),
278
+ text: z.string().optional().describe('Texto interno do shape'),
279
+ color: ColorEnum.default('BLUE'),
280
+ x: z.number().optional(),
281
+ y: z.number().optional(),
282
+ width: z.number().optional(),
283
+ height: z.number().optional(),
284
+ });
285
+
286
+ const ConnectorItem = z.object({
287
+ startNodeId: z.string().describe('ID do nó de origem'),
288
+ endNodeId: z.string().describe('ID do nó de destino'),
289
+ startMagnet: MagnetEnum.default('AUTO'),
290
+ endMagnet: MagnetEnum.default('AUTO'),
291
+ label: z.string().optional().describe('Texto sobre o conector (ex.: Sim/Não)'),
292
+ lineType: LineTypeEnum.optional(),
293
+ startCap: StrokeCapEnum.optional(),
294
+ endCap: StrokeCapEnum.optional(),
295
+ strokeWeight: z.number().optional(),
296
+ color: ColorEnum.optional(),
297
+ });
298
+
299
+ server.tool(
300
+ 'figjam_create_shape',
301
+ 'Cria uma forma com texto no FigJam (BPMN: ELLIPSE=evento, ROUNDED_RECTANGLE=tarefa, DIAMOND=gateway). Retorna nodeId para conectar.',
302
+ ShapeItem.shape,
303
+ async (params) => {
304
+ const result = await sendToPlugin('CREATE_SHAPE', params);
305
+ return { content: [{ type: 'text', text: JSON.stringify(result) }] };
306
+ }
307
+ );
308
+
309
+ server.tool(
310
+ 'figjam_create_shapes',
311
+ 'Cria múltiplas formas com texto de uma vez. Máximo 50 por chamada. Use com figjam_create_connectors para montar fluxos BPMN.',
312
+ {
313
+ shapes: z.array(ShapeItem).max(50).describe('Lista de formas para criar'),
314
+ },
315
+ async ({ shapes }) => {
316
+ const result = await sendToPlugin('CREATE_SHAPES', { shapes });
317
+ return { content: [{ type: 'text', text: JSON.stringify(result) }] };
318
+ }
319
+ );
320
+
321
+ server.tool(
322
+ 'figjam_create_connector',
323
+ 'Conecta dois nós existentes com uma seta (BPMN sequence flow). Aceita nodeId de stickies, shapes ou qualquer SceneNode.',
324
+ ConnectorItem.shape,
325
+ async (params) => {
326
+ const result = await sendToPlugin('CREATE_CONNECTOR', params);
327
+ return { content: [{ type: 'text', text: JSON.stringify(result) }] };
328
+ }
329
+ );
330
+
331
+ server.tool(
332
+ 'figjam_create_connectors',
333
+ 'Cria múltiplos conectores de uma vez. Máximo 100 por chamada. Use após figjam_create_shapes para montar fluxos completos.',
334
+ {
335
+ connectors: z.array(ConnectorItem).max(100).describe('Lista de conectores'),
336
+ },
337
+ async ({ connectors }) => {
338
+ const result = await sendToPlugin('CREATE_CONNECTORS', { connectors });
339
+ return { content: [{ type: 'text', text: JSON.stringify(result) }] };
340
+ }
341
+ );
342
+
343
+ server.tool(
344
+ 'figjam_create_flow',
345
+ 'Atalho: cria um fluxo linear (shapes em sequência horizontal já conectados). Para fluxos com ramificações use figjam_create_shapes + figjam_create_connectors.',
346
+ {
347
+ steps: z.array(z.object({
348
+ label: z.string(),
349
+ shape: ShapeTypeEnum.optional(),
350
+ color: ColorEnum.optional(),
351
+ })).describe('Etapas do fluxo, da esquerda para a direita'),
352
+ x: z.number().optional().describe('X inicial do primeiro shape'),
353
+ y: z.number().optional().describe('Y comum a todos os shapes'),
354
+ gap: z.number().default(220).describe('Distância horizontal entre shapes'),
355
+ },
356
+ async ({ steps, x, y, gap }) => {
357
+ const result = await sendToPlugin('CREATE_FLOW', { steps, x, y, gap });
358
+ return { content: [{ type: 'text', text: JSON.stringify(result) }] };
359
+ }
360
+ );
361
+
362
+ server.tool(
363
+ 'figjam_set_connector_labels',
364
+ 'Define ou atualiza rótulos em conectores existentes (ex.: Sim/Não em gateways BPMN).',
365
+ {
366
+ labels: z.array(z.object({
367
+ nodeId: z.string(),
368
+ label: z.string(),
369
+ })).describe('Pares { nodeId, label } para conectores existentes'),
370
+ },
371
+ async ({ labels }) => {
372
+ const result = await sendToPlugin('SET_CONNECTOR_LABELS', { labels });
373
+ return { content: [{ type: 'text', text: JSON.stringify(result) }] };
374
+ }
375
+ );
376
+
377
+ server.tool(
378
+ 'figjam_move_nodes',
379
+ 'Reposiciona nós existentes pelo ID.',
380
+ {
381
+ moves: z.array(z.object({
382
+ nodeId: z.string(),
383
+ x: z.number(),
384
+ y: z.number(),
385
+ })),
386
+ },
387
+ async ({ moves }) => {
388
+ const result = await sendToPlugin('MOVE_NODES', { moves });
389
+ return { content: [{ type: 'text', text: JSON.stringify(result) }] };
390
+ }
391
+ );
392
+
393
+ server.tool(
394
+ 'figjam_delete_nodes',
395
+ 'Remove nós do canvas pelo ID.',
396
+ {
397
+ nodeIds: z.array(z.string()),
398
+ },
399
+ async ({ nodeIds }) => {
400
+ const result = await sendToPlugin('DELETE_NODES', { nodeIds });
401
+ return { content: [{ type: 'text', text: JSON.stringify(result) }] };
402
+ }
403
+ );
404
+
405
+ // Generic forwarder — lets new plugin operations be invoked without bridge updates.
406
+ // Useful for evolving the plugin independently: any new op type added in code.ts
407
+ // becomes immediately accessible via this tool.
408
+ server.tool(
409
+ 'figjam_execute',
410
+ 'Encaminha uma operação arbitrária para o plugin (escape hatch para ops novas que ainda não têm tool dedicada). Tipos válidos atuais: PING, CREATE_STICKY, CREATE_STICKIES, CREATE_SECTION, CREATE_SHAPE, CREATE_SHAPES, CREATE_CONNECTOR, CREATE_CONNECTORS, CREATE_FLOW, SET_CONNECTOR_LABELS, MOVE_NODES, DELETE_NODES, READ_BOARD, GET_SELECTION.',
411
+ {
412
+ type: z.string().describe('Nome da operação no plugin (ex.: CREATE_SHAPE)'),
413
+ payload: z.record(z.unknown()).optional().describe('Payload da operação'),
414
+ },
415
+ async ({ type, payload }) => {
416
+ const result = await sendToPlugin(type, payload ?? {});
417
+ return { content: [{ type: 'text', text: JSON.stringify(result) }] };
418
+ }
419
+ );
420
+
253
421
  // ── Boot ──────────────────────────────────────────────────────────────────────
254
422
 
255
423
  const existing = await detectExistingBridge();
package/src/index.js DELETED
@@ -1,125 +0,0 @@
1
- #!/usr/bin/env node
2
- import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
- import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
- import { WebSocketServer, WebSocket } from 'ws';
5
- import { createServer } from 'http';
6
- import { z } from 'zod';
7
- // ── WebSocket bridge ──────────────────────────────────────────────────────────
8
- let pluginSocket = null;
9
- const pending = new Map();
10
- function sendToPlugin(type, payload) {
11
- return new Promise((resolve, reject) => {
12
- if (!pluginSocket || pluginSocket.readyState !== WebSocket.OPEN) {
13
- return reject(new Error('Plugin não conectado. Abra o IngresseFlow no Figma Desktop (FigJam).'));
14
- }
15
- const id = crypto.randomUUID();
16
- const timer = setTimeout(() => {
17
- pending.delete(id);
18
- reject(new Error(`Timeout aguardando resposta para ${type}`));
19
- }, 15000);
20
- pending.set(id, {
21
- resolve: (v) => { clearTimeout(timer); resolve(v); },
22
- reject: (e) => { clearTimeout(timer); reject(e); },
23
- timer,
24
- });
25
- pluginSocket.send(JSON.stringify({ id, type, payload }));
26
- });
27
- }
28
- async function tryPort(port) {
29
- return new Promise(resolve => {
30
- const srv = createServer();
31
- srv.once('error', () => resolve(false));
32
- srv.listen(port, () => srv.close(() => resolve(true)));
33
- });
34
- }
35
- async function startBridge() {
36
- const BASE_PORT = 9243;
37
- const MAX_PORT = 9250;
38
- let port = BASE_PORT;
39
- while (port <= MAX_PORT) {
40
- if (await tryPort(port))
41
- break;
42
- port++;
43
- }
44
- if (port > MAX_PORT)
45
- throw new Error('Nenhuma porta livre em 9243–9250');
46
- const wss = new WebSocketServer({ port });
47
- wss.on('connection', (ws) => {
48
- pluginSocket = ws;
49
- log(`Plugin conectado`);
50
- ws.on('message', (raw) => {
51
- try {
52
- const msg = JSON.parse(raw.toString());
53
- const p = pending.get(msg.id);
54
- if (!p)
55
- return;
56
- pending.delete(msg.id);
57
- if (msg.success)
58
- p.resolve(msg.data);
59
- else
60
- p.reject(new Error(msg.error ?? 'Erro desconhecido'));
61
- }
62
- catch (e) {
63
- log(`Erro ao parsear mensagem: ${e}`);
64
- }
65
- });
66
- ws.on('close', () => {
67
- pluginSocket = null;
68
- log(`Plugin desconectado`);
69
- });
70
- });
71
- log(`Bridge escutando em ws://localhost:${port}`);
72
- log(`Abra o plugin IngresseFlow no Figma Desktop para conectar`);
73
- return port;
74
- }
75
- // ── MCP tools ─────────────────────────────────────────────────────────────────
76
- const ColorEnum = z.enum([
77
- 'YELLOW', 'BLUE', 'GREEN', 'PINK', 'ORANGE',
78
- 'PURPLE', 'RED', 'GRAY', 'LIGHT_GRAY',
79
- ]);
80
- const server = new McpServer({ name: 'ingresseflow', version: '1.0.0' });
81
- server.tool('figjam_create_sticky', 'Cria um post-it no FigJam. Requer o plugin IngresseFlow aberto no Figma Desktop.', {
82
- text: z.string().describe('Texto do post-it'),
83
- color: ColorEnum.default('YELLOW').describe('Cor de fundo'),
84
- x: z.number().optional().describe('Posição X no canvas'),
85
- y: z.number().optional().describe('Posição Y no canvas'),
86
- }, async ({ text, color, x, y }) => {
87
- const result = await sendToPlugin('CREATE_STICKY', { text, color, x, y });
88
- return { content: [{ type: 'text', text: JSON.stringify(result) }] };
89
- });
90
- server.tool('figjam_create_stickies', 'Cria múltiplos post-its de uma vez no FigJam. Máximo 50 por chamada. Use para documentar análises completas.', {
91
- stickies: z.array(z.object({
92
- text: z.string(),
93
- color: ColorEnum.default('YELLOW'),
94
- x: z.number().optional(),
95
- y: z.number().optional(),
96
- })).max(50).describe('Lista de post-its para criar'),
97
- }, async ({ stickies }) => {
98
- const result = await sendToPlugin('CREATE_STICKIES', { stickies });
99
- return { content: [{ type: 'text', text: JSON.stringify(result) }] };
100
- });
101
- server.tool('figjam_read_board', 'Lê todo o conteúdo do FigJam atual. Retorna nós com texto, posição e tipo.', {}, async () => {
102
- const result = await sendToPlugin('READ_BOARD', {});
103
- return { content: [{ type: 'text', text: JSON.stringify(result) }] };
104
- });
105
- server.tool('figjam_get_selection', 'Retorna os nós atualmente selecionados no Figma.', {}, async () => {
106
- const result = await sendToPlugin('GET_SELECTION', {});
107
- return { content: [{ type: 'text', text: JSON.stringify(result) }] };
108
- });
109
- server.tool('figjam_create_section', 'Cria uma seção nomeada no FigJam para agrupar conteúdo.', {
110
- name: z.string().describe('Nome da seção'),
111
- x: z.number().optional().describe('Posição X'),
112
- y: z.number().optional().describe('Posição Y'),
113
- width: z.number().default(900).describe('Largura em px'),
114
- height: z.number().default(700).describe('Altura em px'),
115
- }, async ({ name, x, y, width, height }) => {
116
- const result = await sendToPlugin('CREATE_SECTION', { name, x, y, width, height });
117
- return { content: [{ type: 'text', text: JSON.stringify(result) }] };
118
- });
119
- // ── Boot ──────────────────────────────────────────────────────────────────────
120
- function log(msg) {
121
- process.stderr.write(`[IngresseFlow] ${msg}\n`);
122
- }
123
- await startBridge();
124
- const transport = new StdioServerTransport();
125
- await server.connect(transport);