ingresseflow-bridge 1.0.2 → 1.2.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
@@ -179,10 +179,44 @@ server.tool('figjam_create_stickies', 'Cria múltiplos post-its de uma vez no Fi
179
179
  const result = await sendToPlugin('CREATE_STICKIES', { stickies });
180
180
  return { content: [{ type: 'text', text: JSON.stringify(result) }] };
181
181
  });
182
- server.tool('figjam_read_board', 'Lê todo o conteúdo do FigJam atual. Retorna nós com texto, posição e tipo.', {}, async () => {
183
- const result = await sendToPlugin('READ_BOARD', {});
182
+ server.tool('figjam_read_board', 'Lê o conteúdo do FigJam. Por padrão faz recursão dentro de sections/frames/groups. Retorna id, type, name, x, y, width, height, parentId, depth, text (se STICKY/TEXT/SHAPE_WITH_TEXT/CONNECTOR) e hasImage=true para nós com fill de imagem.', {
183
+ recursive: z.boolean().default(true).describe('Descer dentro de containers (section, frame, group)'),
184
+ maxDepth: z.number().default(10).describe('Profundidade máxima de recursão'),
185
+ filterTypes: z.array(z.string()).optional().describe('Filtra os nós retornados por tipo (ex.: ["STICKY","RECTANGLE","FRAME"]). A recursão continua mesmo em nós filtrados.'),
186
+ }, async ({ recursive, maxDepth, filterTypes }) => {
187
+ const result = await sendToPlugin('READ_BOARD', { recursive, maxDepth, filterTypes });
184
188
  return { content: [{ type: 'text', text: JSON.stringify(result) }] };
185
189
  });
190
+ server.tool('figjam_export_node', 'Exporta um nó (frame, image, section, qualquer SceneNode com exportAsync) como PNG/JPG/SVG/PDF e retorna a imagem renderizada para que Claude consiga visualizar prints/screenshots colados no canvas.', {
191
+ nodeId: z.string().describe('ID do nó a exportar'),
192
+ format: z.enum(['PNG', 'JPG', 'SVG', 'PDF']).default('PNG'),
193
+ scale: z.number().default(1).describe('Escala da exportação (1=original, 2=2x). Ignorado em SVG/PDF.'),
194
+ }, async ({ nodeId, format, scale }) => {
195
+ const result = await sendToPlugin('EXPORT_NODE', { nodeId, format, scale });
196
+ const mimeMap = {
197
+ PNG: 'image/png',
198
+ JPG: 'image/jpeg',
199
+ SVG: 'image/svg+xml',
200
+ PDF: 'application/pdf',
201
+ };
202
+ const mimeType = mimeMap[result.format] ?? 'image/png';
203
+ // Image formats Claude can render get image content; SVG/PDF fall back to text.
204
+ if (result.format === 'PNG' || result.format === 'JPG') {
205
+ return {
206
+ content: [
207
+ { type: 'image', data: result.base64, mimeType },
208
+ { type: 'text', text: JSON.stringify({
209
+ nodeId, format: result.format,
210
+ width: result.width, height: result.height,
211
+ byteLength: result.byteLength,
212
+ }) },
213
+ ],
214
+ };
215
+ }
216
+ return {
217
+ content: [{ type: 'text', text: JSON.stringify(result) }],
218
+ };
219
+ });
186
220
  server.tool('figjam_get_selection', 'Retorna os nós atualmente selecionados no Figma.', {}, async () => {
187
221
  const result = await sendToPlugin('GET_SELECTION', {});
188
222
  return { content: [{ type: 'text', text: JSON.stringify(result) }] };
@@ -197,6 +231,113 @@ server.tool('figjam_create_section', 'Cria uma seção nomeada no FigJam para ag
197
231
  const result = await sendToPlugin('CREATE_SECTION', { name, x, y, width, height });
198
232
  return { content: [{ type: 'text', text: JSON.stringify(result) }] };
199
233
  });
234
+ // ── Shapes & connectors (BPMN-friendly) ───────────────────────────────────────
235
+ const ShapeTypeEnum = z.enum([
236
+ 'SQUARE', 'ELLIPSE', 'ROUNDED_RECTANGLE', 'DIAMOND',
237
+ 'TRIANGLE_UP', 'TRIANGLE_DOWN',
238
+ 'PARALLELOGRAM_RIGHT', 'PARALLELOGRAM_LEFT',
239
+ 'ENG_DATABASE', 'ENG_QUEUE', 'ENG_FILE', 'ENG_FOLDER',
240
+ 'TRAPEZOID', 'PREDEFINED_PROCESS', 'SHIELD',
241
+ 'DOCUMENT_SINGLE', 'DOCUMENT_MULTIPLE', 'MANUAL_INPUT',
242
+ 'HEXAGON', 'CHEVRON_RIGHT_ARROW', 'CHEVRON_RIGHT_PENTAGON',
243
+ ]).describe('Tipo de shape (BPMN: ELLIPSE=evento, ROUNDED_RECTANGLE=tarefa, DIAMOND=gateway)');
244
+ const MagnetEnum = z.enum(['AUTO', 'TOP', 'LEFT', 'BOTTOM', 'RIGHT', 'CENTER'])
245
+ .describe('Ponto de ancoragem do conector no nó');
246
+ const StrokeCapEnum = z.enum([
247
+ 'NONE', 'ARROW_LINES', 'ARROW_EQUILATERAL',
248
+ 'TRIANGLE_FILLED', 'DIAMOND_FILLED', 'CIRCLE_FILLED',
249
+ ]).describe('Estilo da ponta do conector');
250
+ const LineTypeEnum = z.enum(['STRAIGHT', 'ELBOWED'])
251
+ .describe('Reta direta ou com cotovelos (90°)');
252
+ const ShapeItem = z.object({
253
+ shape: ShapeTypeEnum.default('ROUNDED_RECTANGLE'),
254
+ text: z.string().optional().describe('Texto interno do shape'),
255
+ color: ColorEnum.default('BLUE'),
256
+ x: z.number().optional(),
257
+ y: z.number().optional(),
258
+ width: z.number().optional(),
259
+ height: z.number().optional(),
260
+ });
261
+ const ConnectorItem = z.object({
262
+ startNodeId: z.string().describe('ID do nó de origem'),
263
+ endNodeId: z.string().describe('ID do nó de destino'),
264
+ startMagnet: MagnetEnum.default('AUTO'),
265
+ endMagnet: MagnetEnum.default('AUTO'),
266
+ label: z.string().optional().describe('Texto sobre o conector (ex.: Sim/Não)'),
267
+ lineType: LineTypeEnum.optional(),
268
+ startCap: StrokeCapEnum.optional(),
269
+ endCap: StrokeCapEnum.optional(),
270
+ strokeWeight: z.number().optional(),
271
+ color: ColorEnum.optional(),
272
+ });
273
+ 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) => {
274
+ const result = await sendToPlugin('CREATE_SHAPE', params);
275
+ return { content: [{ type: 'text', text: JSON.stringify(result) }] };
276
+ });
277
+ 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.', {
278
+ shapes: z.array(ShapeItem).max(50).describe('Lista de formas para criar'),
279
+ }, async ({ shapes }) => {
280
+ const result = await sendToPlugin('CREATE_SHAPES', { shapes });
281
+ return { content: [{ type: 'text', text: JSON.stringify(result) }] };
282
+ });
283
+ 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) => {
284
+ const result = await sendToPlugin('CREATE_CONNECTOR', params);
285
+ return { content: [{ type: 'text', text: JSON.stringify(result) }] };
286
+ });
287
+ 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.', {
288
+ connectors: z.array(ConnectorItem).max(100).describe('Lista de conectores'),
289
+ }, async ({ connectors }) => {
290
+ const result = await sendToPlugin('CREATE_CONNECTORS', { connectors });
291
+ return { content: [{ type: 'text', text: JSON.stringify(result) }] };
292
+ });
293
+ 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.', {
294
+ steps: z.array(z.object({
295
+ label: z.string(),
296
+ shape: ShapeTypeEnum.optional(),
297
+ color: ColorEnum.optional(),
298
+ })).describe('Etapas do fluxo, da esquerda para a direita'),
299
+ x: z.number().optional().describe('X inicial do primeiro shape'),
300
+ y: z.number().optional().describe('Y comum a todos os shapes'),
301
+ gap: z.number().default(220).describe('Distância horizontal entre shapes'),
302
+ }, async ({ steps, x, y, gap }) => {
303
+ const result = await sendToPlugin('CREATE_FLOW', { steps, x, y, gap });
304
+ return { content: [{ type: 'text', text: JSON.stringify(result) }] };
305
+ });
306
+ server.tool('figjam_set_connector_labels', 'Define ou atualiza rótulos em conectores existentes (ex.: Sim/Não em gateways BPMN).', {
307
+ labels: z.array(z.object({
308
+ nodeId: z.string(),
309
+ label: z.string(),
310
+ })).describe('Pares { nodeId, label } para conectores existentes'),
311
+ }, async ({ labels }) => {
312
+ const result = await sendToPlugin('SET_CONNECTOR_LABELS', { labels });
313
+ return { content: [{ type: 'text', text: JSON.stringify(result) }] };
314
+ });
315
+ server.tool('figjam_move_nodes', 'Reposiciona nós existentes pelo ID.', {
316
+ moves: z.array(z.object({
317
+ nodeId: z.string(),
318
+ x: z.number(),
319
+ y: z.number(),
320
+ })),
321
+ }, async ({ moves }) => {
322
+ const result = await sendToPlugin('MOVE_NODES', { moves });
323
+ return { content: [{ type: 'text', text: JSON.stringify(result) }] };
324
+ });
325
+ server.tool('figjam_delete_nodes', 'Remove nós do canvas pelo ID.', {
326
+ nodeIds: z.array(z.string()),
327
+ }, async ({ nodeIds }) => {
328
+ const result = await sendToPlugin('DELETE_NODES', { nodeIds });
329
+ return { content: [{ type: 'text', text: JSON.stringify(result) }] };
330
+ });
331
+ // Generic forwarder — lets new plugin operations be invoked without bridge updates.
332
+ // Useful for evolving the plugin independently: any new op type added in code.ts
333
+ // becomes immediately accessible via this tool.
334
+ 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, EXPORT_NODE.', {
335
+ type: z.string().describe('Nome da operação no plugin (ex.: CREATE_SHAPE)'),
336
+ payload: z.record(z.unknown()).optional().describe('Payload da operação'),
337
+ }, async ({ type, payload }) => {
338
+ const result = await sendToPlugin(type, payload ?? {});
339
+ return { content: [{ type: 'text', text: JSON.stringify(result) }] };
340
+ });
200
341
  // ── Boot ──────────────────────────────────────────────────────────────────────
201
342
  const existing = await detectExistingBridge();
202
343
  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.2.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
@@ -216,14 +216,57 @@ server.tool(
216
216
 
217
217
  server.tool(
218
218
  'figjam_read_board',
219
- 'Lê todo o conteúdo do FigJam atual. Retorna nós com texto, posição e tipo.',
220
- {},
221
- async () => {
222
- const result = await sendToPlugin('READ_BOARD', {});
219
+ 'Lê o conteúdo do FigJam. Por padrão faz recursão dentro de sections/frames/groups. Retorna id, type, name, x, y, width, height, parentId, depth, text (se STICKY/TEXT/SHAPE_WITH_TEXT/CONNECTOR) e hasImage=true para nós com fill de imagem.',
220
+ {
221
+ recursive: z.boolean().default(true).describe('Descer dentro de containers (section, frame, group)'),
222
+ maxDepth: z.number().default(10).describe('Profundidade máxima de recursão'),
223
+ filterTypes: z.array(z.string()).optional().describe('Filtra os nós retornados por tipo (ex.: ["STICKY","RECTANGLE","FRAME"]). A recursão continua mesmo em nós filtrados.'),
224
+ },
225
+ async ({ recursive, maxDepth, filterTypes }) => {
226
+ const result = await sendToPlugin('READ_BOARD', { recursive, maxDepth, filterTypes });
223
227
  return { content: [{ type: 'text', text: JSON.stringify(result) }] };
224
228
  }
225
229
  );
226
230
 
231
+ server.tool(
232
+ 'figjam_export_node',
233
+ 'Exporta um nó (frame, image, section, qualquer SceneNode com exportAsync) como PNG/JPG/SVG/PDF e retorna a imagem renderizada para que Claude consiga visualizar prints/screenshots colados no canvas.',
234
+ {
235
+ nodeId: z.string().describe('ID do nó a exportar'),
236
+ format: z.enum(['PNG', 'JPG', 'SVG', 'PDF']).default('PNG'),
237
+ scale: z.number().default(1).describe('Escala da exportação (1=original, 2=2x). Ignorado em SVG/PDF.'),
238
+ },
239
+ async ({ nodeId, format, scale }) => {
240
+ const result = await sendToPlugin('EXPORT_NODE', { nodeId, format, scale }) as {
241
+ base64: string; format: string; width: number; height: number; byteLength: number;
242
+ };
243
+ const mimeMap: Record<string, string> = {
244
+ PNG: 'image/png',
245
+ JPG: 'image/jpeg',
246
+ SVG: 'image/svg+xml',
247
+ PDF: 'application/pdf',
248
+ };
249
+ const mimeType = mimeMap[result.format] ?? 'image/png';
250
+
251
+ // Image formats Claude can render get image content; SVG/PDF fall back to text.
252
+ if (result.format === 'PNG' || result.format === 'JPG') {
253
+ return {
254
+ content: [
255
+ { type: 'image', data: result.base64, mimeType },
256
+ { type: 'text', text: JSON.stringify({
257
+ nodeId, format: result.format,
258
+ width: result.width, height: result.height,
259
+ byteLength: result.byteLength,
260
+ }) },
261
+ ],
262
+ };
263
+ }
264
+ return {
265
+ content: [{ type: 'text', text: JSON.stringify(result) }],
266
+ };
267
+ }
268
+ );
269
+
227
270
  server.tool(
228
271
  'figjam_get_selection',
229
272
  'Retorna os nós atualmente selecionados no Figma.',
@@ -250,6 +293,174 @@ server.tool(
250
293
  }
251
294
  );
252
295
 
296
+ // ── Shapes & connectors (BPMN-friendly) ───────────────────────────────────────
297
+
298
+ const ShapeTypeEnum = z.enum([
299
+ 'SQUARE', 'ELLIPSE', 'ROUNDED_RECTANGLE', 'DIAMOND',
300
+ 'TRIANGLE_UP', 'TRIANGLE_DOWN',
301
+ 'PARALLELOGRAM_RIGHT', 'PARALLELOGRAM_LEFT',
302
+ 'ENG_DATABASE', 'ENG_QUEUE', 'ENG_FILE', 'ENG_FOLDER',
303
+ 'TRAPEZOID', 'PREDEFINED_PROCESS', 'SHIELD',
304
+ 'DOCUMENT_SINGLE', 'DOCUMENT_MULTIPLE', 'MANUAL_INPUT',
305
+ 'HEXAGON', 'CHEVRON_RIGHT_ARROW', 'CHEVRON_RIGHT_PENTAGON',
306
+ ]).describe('Tipo de shape (BPMN: ELLIPSE=evento, ROUNDED_RECTANGLE=tarefa, DIAMOND=gateway)');
307
+
308
+ const MagnetEnum = z.enum(['AUTO', 'TOP', 'LEFT', 'BOTTOM', 'RIGHT', 'CENTER'])
309
+ .describe('Ponto de ancoragem do conector no nó');
310
+
311
+ const StrokeCapEnum = z.enum([
312
+ 'NONE', 'ARROW_LINES', 'ARROW_EQUILATERAL',
313
+ 'TRIANGLE_FILLED', 'DIAMOND_FILLED', 'CIRCLE_FILLED',
314
+ ]).describe('Estilo da ponta do conector');
315
+
316
+ const LineTypeEnum = z.enum(['STRAIGHT', 'ELBOWED'])
317
+ .describe('Reta direta ou com cotovelos (90°)');
318
+
319
+ const ShapeItem = z.object({
320
+ shape: ShapeTypeEnum.default('ROUNDED_RECTANGLE'),
321
+ text: z.string().optional().describe('Texto interno do shape'),
322
+ color: ColorEnum.default('BLUE'),
323
+ x: z.number().optional(),
324
+ y: z.number().optional(),
325
+ width: z.number().optional(),
326
+ height: z.number().optional(),
327
+ });
328
+
329
+ const ConnectorItem = z.object({
330
+ startNodeId: z.string().describe('ID do nó de origem'),
331
+ endNodeId: z.string().describe('ID do nó de destino'),
332
+ startMagnet: MagnetEnum.default('AUTO'),
333
+ endMagnet: MagnetEnum.default('AUTO'),
334
+ label: z.string().optional().describe('Texto sobre o conector (ex.: Sim/Não)'),
335
+ lineType: LineTypeEnum.optional(),
336
+ startCap: StrokeCapEnum.optional(),
337
+ endCap: StrokeCapEnum.optional(),
338
+ strokeWeight: z.number().optional(),
339
+ color: ColorEnum.optional(),
340
+ });
341
+
342
+ server.tool(
343
+ 'figjam_create_shape',
344
+ 'Cria uma forma com texto no FigJam (BPMN: ELLIPSE=evento, ROUNDED_RECTANGLE=tarefa, DIAMOND=gateway). Retorna nodeId para conectar.',
345
+ ShapeItem.shape,
346
+ async (params) => {
347
+ const result = await sendToPlugin('CREATE_SHAPE', params);
348
+ return { content: [{ type: 'text', text: JSON.stringify(result) }] };
349
+ }
350
+ );
351
+
352
+ server.tool(
353
+ 'figjam_create_shapes',
354
+ 'Cria múltiplas formas com texto de uma vez. Máximo 50 por chamada. Use com figjam_create_connectors para montar fluxos BPMN.',
355
+ {
356
+ shapes: z.array(ShapeItem).max(50).describe('Lista de formas para criar'),
357
+ },
358
+ async ({ shapes }) => {
359
+ const result = await sendToPlugin('CREATE_SHAPES', { shapes });
360
+ return { content: [{ type: 'text', text: JSON.stringify(result) }] };
361
+ }
362
+ );
363
+
364
+ server.tool(
365
+ 'figjam_create_connector',
366
+ 'Conecta dois nós existentes com uma seta (BPMN sequence flow). Aceita nodeId de stickies, shapes ou qualquer SceneNode.',
367
+ ConnectorItem.shape,
368
+ async (params) => {
369
+ const result = await sendToPlugin('CREATE_CONNECTOR', params);
370
+ return { content: [{ type: 'text', text: JSON.stringify(result) }] };
371
+ }
372
+ );
373
+
374
+ server.tool(
375
+ 'figjam_create_connectors',
376
+ 'Cria múltiplos conectores de uma vez. Máximo 100 por chamada. Use após figjam_create_shapes para montar fluxos completos.',
377
+ {
378
+ connectors: z.array(ConnectorItem).max(100).describe('Lista de conectores'),
379
+ },
380
+ async ({ connectors }) => {
381
+ const result = await sendToPlugin('CREATE_CONNECTORS', { connectors });
382
+ return { content: [{ type: 'text', text: JSON.stringify(result) }] };
383
+ }
384
+ );
385
+
386
+ server.tool(
387
+ 'figjam_create_flow',
388
+ 'Atalho: cria um fluxo linear (shapes em sequência horizontal já conectados). Para fluxos com ramificações use figjam_create_shapes + figjam_create_connectors.',
389
+ {
390
+ steps: z.array(z.object({
391
+ label: z.string(),
392
+ shape: ShapeTypeEnum.optional(),
393
+ color: ColorEnum.optional(),
394
+ })).describe('Etapas do fluxo, da esquerda para a direita'),
395
+ x: z.number().optional().describe('X inicial do primeiro shape'),
396
+ y: z.number().optional().describe('Y comum a todos os shapes'),
397
+ gap: z.number().default(220).describe('Distância horizontal entre shapes'),
398
+ },
399
+ async ({ steps, x, y, gap }) => {
400
+ const result = await sendToPlugin('CREATE_FLOW', { steps, x, y, gap });
401
+ return { content: [{ type: 'text', text: JSON.stringify(result) }] };
402
+ }
403
+ );
404
+
405
+ server.tool(
406
+ 'figjam_set_connector_labels',
407
+ 'Define ou atualiza rótulos em conectores existentes (ex.: Sim/Não em gateways BPMN).',
408
+ {
409
+ labels: z.array(z.object({
410
+ nodeId: z.string(),
411
+ label: z.string(),
412
+ })).describe('Pares { nodeId, label } para conectores existentes'),
413
+ },
414
+ async ({ labels }) => {
415
+ const result = await sendToPlugin('SET_CONNECTOR_LABELS', { labels });
416
+ return { content: [{ type: 'text', text: JSON.stringify(result) }] };
417
+ }
418
+ );
419
+
420
+ server.tool(
421
+ 'figjam_move_nodes',
422
+ 'Reposiciona nós existentes pelo ID.',
423
+ {
424
+ moves: z.array(z.object({
425
+ nodeId: z.string(),
426
+ x: z.number(),
427
+ y: z.number(),
428
+ })),
429
+ },
430
+ async ({ moves }) => {
431
+ const result = await sendToPlugin('MOVE_NODES', { moves });
432
+ return { content: [{ type: 'text', text: JSON.stringify(result) }] };
433
+ }
434
+ );
435
+
436
+ server.tool(
437
+ 'figjam_delete_nodes',
438
+ 'Remove nós do canvas pelo ID.',
439
+ {
440
+ nodeIds: z.array(z.string()),
441
+ },
442
+ async ({ nodeIds }) => {
443
+ const result = await sendToPlugin('DELETE_NODES', { nodeIds });
444
+ return { content: [{ type: 'text', text: JSON.stringify(result) }] };
445
+ }
446
+ );
447
+
448
+ // Generic forwarder — lets new plugin operations be invoked without bridge updates.
449
+ // Useful for evolving the plugin independently: any new op type added in code.ts
450
+ // becomes immediately accessible via this tool.
451
+ server.tool(
452
+ 'figjam_execute',
453
+ '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, EXPORT_NODE.',
454
+ {
455
+ type: z.string().describe('Nome da operação no plugin (ex.: CREATE_SHAPE)'),
456
+ payload: z.record(z.unknown()).optional().describe('Payload da operação'),
457
+ },
458
+ async ({ type, payload }) => {
459
+ const result = await sendToPlugin(type, payload ?? {});
460
+ return { content: [{ type: 'text', text: JSON.stringify(result) }] };
461
+ }
462
+ );
463
+
253
464
  // ── Boot ──────────────────────────────────────────────────────────────────────
254
465
 
255
466
  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);