ingresseflow-bridge 1.0.1 → 1.0.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.
Files changed (3) hide show
  1. package/dist/index.js +84 -34
  2. package/package.json +1 -1
  3. package/src/index.ts +90 -36
package/dist/index.js CHANGED
@@ -14,10 +14,40 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
14
14
  import { WebSocketServer, WebSocket } from 'ws';
15
15
  import { createServer } from 'http';
16
16
  import { z } from 'zod';
17
- // ── WebSocket bridge ──────────────────────────────────────────────────────────
17
+ const HTTP_PORT = 9242;
18
+ function log(msg) {
19
+ process.stderr.write(`[IngresseFlow] ${msg}\n`);
20
+ }
21
+ // ── Bridge state ──────────────────────────────────────────────────────────────
22
+ // Two boot modes:
23
+ // - 'server': this process owns the WebSocket bridge and HTTP command server.
24
+ // - 'client': another bridge already owns :9242 (typically the LaunchAgent).
25
+ // This process is just an MCP front-end and forwards calls via HTTP.
26
+ // The client mode prevents EADDRINUSE crashes when Claude Code spawns a stdio
27
+ // MCP child while the persistent bridge is already running.
28
+ let mode = 'server';
18
29
  let pluginSocket = null;
19
30
  const pending = new Map();
31
+ async function detectExistingBridge() {
32
+ // If :9242 is occupied, another bridge owns it — go into client mode.
33
+ // tryPort returns false when listen fails (EADDRINUSE), which is exactly
34
+ // the signal we need; works even against older bridges without /health.
35
+ return !(await tryPort(HTTP_PORT));
36
+ }
37
+ async function forwardViaHttp(type, payload) {
38
+ const r = await fetch(`http://localhost:${HTTP_PORT}/command`, {
39
+ method: 'POST',
40
+ headers: { 'Content-Type': 'application/json' },
41
+ body: JSON.stringify({ type, payload }),
42
+ });
43
+ const json = await r.json();
44
+ if (!json.success)
45
+ throw new Error(json.error ?? 'Erro desconhecido');
46
+ return json.data;
47
+ }
20
48
  function sendToPlugin(type, payload) {
49
+ if (mode === 'client')
50
+ return forwardViaHttp(type, payload);
21
51
  return new Promise((resolve, reject) => {
22
52
  if (!pluginSocket || pluginSocket.readyState !== WebSocket.OPEN) {
23
53
  return reject(new Error('Plugin não conectado. Abra o IngresseFlow no Figma Desktop (FigJam).'));
@@ -35,14 +65,14 @@ function sendToPlugin(type, payload) {
35
65
  pluginSocket.send(JSON.stringify({ id, type, payload }));
36
66
  });
37
67
  }
38
- async function tryPort(port) {
68
+ async function tryPort(port, host = '127.0.0.1') {
39
69
  return new Promise(resolve => {
40
70
  const srv = createServer();
41
71
  srv.once('error', () => resolve(false));
42
- srv.listen(port, () => srv.close(() => resolve(true)));
72
+ srv.listen(port, host, () => srv.close(() => resolve(true)));
43
73
  });
44
74
  }
45
- async function startBridge() {
75
+ async function startWebSocketBridge() {
46
76
  const BASE_PORT = 9243;
47
77
  const MAX_PORT = 9250;
48
78
  let port = BASE_PORT;
@@ -54,6 +84,9 @@ async function startBridge() {
54
84
  if (port > MAX_PORT)
55
85
  throw new Error('Nenhuma porta livre em 9243–9250');
56
86
  const wss = new WebSocketServer({ port, host: '127.0.0.1' });
87
+ wss.on('error', (err) => {
88
+ log(`Erro no WebSocketServer: ${err.message}`);
89
+ });
57
90
  wss.on('connection', (ws) => {
58
91
  pluginSocket = ws;
59
92
  log(`Plugin conectado`);
@@ -82,6 +115,44 @@ async function startBridge() {
82
115
  log(`Abra o plugin IngresseFlow no Figma Desktop para conectar`);
83
116
  return port;
84
117
  }
118
+ // ── HTTP command server ──────────────────────────────────────────────────────
119
+ // Exposes:
120
+ // GET /health — used by other bridge instances to detect us and switch
121
+ // to client mode instead of crashing on EADDRINUSE.
122
+ // POST /command — { type, payload } forwarded to the plugin via WebSocket.
123
+ function startCommandServer() {
124
+ const http = createServer(async (req, res) => {
125
+ if (req.method === 'GET' && req.url === '/health') {
126
+ res.writeHead(200, { 'Content-Type': 'application/json' });
127
+ res.end(JSON.stringify({ status: 'ok', plugin: !!pluginSocket }));
128
+ return;
129
+ }
130
+ if (req.method !== 'POST' || req.url !== '/command') {
131
+ res.writeHead(404).end();
132
+ return;
133
+ }
134
+ let body = '';
135
+ req.on('data', (c) => { body += c; });
136
+ req.on('end', async () => {
137
+ try {
138
+ const { type, payload } = JSON.parse(body);
139
+ const result = await sendToPlugin(type, payload);
140
+ res.writeHead(200, { 'Content-Type': 'application/json' });
141
+ res.end(JSON.stringify({ success: true, data: result }));
142
+ }
143
+ catch (err) {
144
+ res.writeHead(200, { 'Content-Type': 'application/json' });
145
+ res.end(JSON.stringify({ success: false, error: err.message }));
146
+ }
147
+ });
148
+ });
149
+ http.on('error', (err) => {
150
+ log(`Erro no HTTP server: ${err.message}`);
151
+ });
152
+ http.listen(HTTP_PORT, '127.0.0.1', () => {
153
+ log(`Comando HTTP em http://localhost:${HTTP_PORT}/command`);
154
+ });
155
+ }
85
156
  // ── MCP tools ─────────────────────────────────────────────────────────────────
86
157
  const ColorEnum = z.enum([
87
158
  'YELLOW', 'BLUE', 'GREEN', 'PINK', 'ORANGE',
@@ -126,37 +197,16 @@ server.tool('figjam_create_section', 'Cria uma seção nomeada no FigJam para ag
126
197
  const result = await sendToPlugin('CREATE_SECTION', { name, x, y, width, height });
127
198
  return { content: [{ type: 'text', text: JSON.stringify(result) }] };
128
199
  });
129
- // ── HTTP command server (port 9242) ───────────────────────────────────────────
130
- // Accepts POST /command { type, payload } — lets any local client send operations
131
- // without needing an MCP session. Used by scripts and tests.
132
- function startCommandServer() {
133
- const http = createServer(async (req, res) => {
134
- if (req.method !== 'POST' || req.url !== '/command') {
135
- res.writeHead(404).end();
136
- return;
137
- }
138
- let body = '';
139
- req.on('data', (c) => { body += c; });
140
- req.on('end', async () => {
141
- try {
142
- const { type, payload } = JSON.parse(body);
143
- const result = await sendToPlugin(type, payload);
144
- res.writeHead(200, { 'Content-Type': 'application/json' });
145
- res.end(JSON.stringify({ success: true, data: result }));
146
- }
147
- catch (err) {
148
- res.writeHead(200, { 'Content-Type': 'application/json' });
149
- res.end(JSON.stringify({ success: false, error: err.message }));
150
- }
151
- });
152
- });
153
- http.listen(9242, '127.0.0.1', () => log('Comando HTTP em http://localhost:9242/command'));
154
- }
155
200
  // ── Boot ──────────────────────────────────────────────────────────────────────
156
- function log(msg) {
157
- process.stderr.write(`[IngresseFlow] ${msg}\n`);
201
+ const existing = await detectExistingBridge();
202
+ if (existing) {
203
+ mode = 'client';
204
+ log(`Bridge persistente detectada em :${HTTP_PORT} — operando em modo cliente.`);
205
+ }
206
+ else {
207
+ mode = 'server';
208
+ await startWebSocketBridge();
209
+ startCommandServer();
158
210
  }
159
- await startBridge();
160
- startCommandServer();
161
211
  const transport = new StdioServerTransport();
162
212
  await server.connect(transport);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ingresseflow-bridge",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
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
@@ -11,8 +11,21 @@ import { WebSocketServer, WebSocket } from 'ws';
11
11
  import { createServer } from 'http';
12
12
  import { z } from 'zod';
13
13
 
14
- // ── WebSocket bridge ──────────────────────────────────────────────────────────
14
+ const HTTP_PORT = 9242;
15
15
 
16
+ function log(msg: string) {
17
+ process.stderr.write(`[IngresseFlow] ${msg}\n`);
18
+ }
19
+
20
+ // ── Bridge state ──────────────────────────────────────────────────────────────
21
+ // Two boot modes:
22
+ // - 'server': this process owns the WebSocket bridge and HTTP command server.
23
+ // - 'client': another bridge already owns :9242 (typically the LaunchAgent).
24
+ // This process is just an MCP front-end and forwards calls via HTTP.
25
+ // The client mode prevents EADDRINUSE crashes when Claude Code spawns a stdio
26
+ // MCP child while the persistent bridge is already running.
27
+
28
+ let mode: 'server' | 'client' = 'server';
16
29
  let pluginSocket: WebSocket | null = null;
17
30
 
18
31
  const pending = new Map<string, {
@@ -21,7 +34,27 @@ const pending = new Map<string, {
21
34
  timer: ReturnType<typeof setTimeout>;
22
35
  }>();
23
36
 
37
+ async function detectExistingBridge(): Promise<boolean> {
38
+ // If :9242 is occupied, another bridge owns it — go into client mode.
39
+ // tryPort returns false when listen fails (EADDRINUSE), which is exactly
40
+ // the signal we need; works even against older bridges without /health.
41
+ return !(await tryPort(HTTP_PORT));
42
+ }
43
+
44
+ async function forwardViaHttp<T>(type: string, payload: unknown): Promise<T> {
45
+ const r = await fetch(`http://localhost:${HTTP_PORT}/command`, {
46
+ method: 'POST',
47
+ headers: { 'Content-Type': 'application/json' },
48
+ body: JSON.stringify({ type, payload }),
49
+ });
50
+ const json = await r.json() as { success: boolean; data?: T; error?: string };
51
+ if (!json.success) throw new Error(json.error ?? 'Erro desconhecido');
52
+ return json.data as T;
53
+ }
54
+
24
55
  function sendToPlugin<T>(type: string, payload: unknown): Promise<T> {
56
+ if (mode === 'client') return forwardViaHttp<T>(type, payload);
57
+
25
58
  return new Promise((resolve, reject) => {
26
59
  if (!pluginSocket || pluginSocket.readyState !== WebSocket.OPEN) {
27
60
  return reject(new Error(
@@ -44,15 +77,15 @@ function sendToPlugin<T>(type: string, payload: unknown): Promise<T> {
44
77
  });
45
78
  }
46
79
 
47
- async function tryPort(port: number): Promise<boolean> {
80
+ async function tryPort(port: number, host = '127.0.0.1'): Promise<boolean> {
48
81
  return new Promise(resolve => {
49
82
  const srv = createServer();
50
83
  srv.once('error', () => resolve(false));
51
- srv.listen(port, () => srv.close(() => resolve(true)));
84
+ srv.listen(port, host, () => srv.close(() => resolve(true)));
52
85
  });
53
86
  }
54
87
 
55
- async function startBridge(): Promise<number> {
88
+ async function startWebSocketBridge(): Promise<number> {
56
89
  const BASE_PORT = 9243;
57
90
  const MAX_PORT = 9250;
58
91
  let port = BASE_PORT;
@@ -65,6 +98,10 @@ async function startBridge(): Promise<number> {
65
98
 
66
99
  const wss = new WebSocketServer({ port, host: '127.0.0.1' });
67
100
 
101
+ wss.on('error', (err) => {
102
+ log(`Erro no WebSocketServer: ${(err as Error).message}`);
103
+ });
104
+
68
105
  wss.on('connection', (ws) => {
69
106
  pluginSocket = ws;
70
107
  log(`Plugin conectado`);
@@ -95,6 +132,47 @@ async function startBridge(): Promise<number> {
95
132
  return port;
96
133
  }
97
134
 
135
+ // ── HTTP command server ──────────────────────────────────────────────────────
136
+ // Exposes:
137
+ // GET /health — used by other bridge instances to detect us and switch
138
+ // to client mode instead of crashing on EADDRINUSE.
139
+ // POST /command — { type, payload } forwarded to the plugin via WebSocket.
140
+
141
+ function startCommandServer() {
142
+ const http = createServer(async (req, res) => {
143
+ if (req.method === 'GET' && req.url === '/health') {
144
+ res.writeHead(200, { 'Content-Type': 'application/json' });
145
+ res.end(JSON.stringify({ status: 'ok', plugin: !!pluginSocket }));
146
+ return;
147
+ }
148
+ if (req.method !== 'POST' || req.url !== '/command') {
149
+ res.writeHead(404).end();
150
+ return;
151
+ }
152
+ let body = '';
153
+ req.on('data', (c: Buffer) => { body += c; });
154
+ req.on('end', async () => {
155
+ try {
156
+ const { type, payload } = JSON.parse(body) as { type: string; payload: unknown };
157
+ const result = await sendToPlugin(type, payload);
158
+ res.writeHead(200, { 'Content-Type': 'application/json' });
159
+ res.end(JSON.stringify({ success: true, data: result }));
160
+ } catch (err) {
161
+ res.writeHead(200, { 'Content-Type': 'application/json' });
162
+ res.end(JSON.stringify({ success: false, error: (err as Error).message }));
163
+ }
164
+ });
165
+ });
166
+
167
+ http.on('error', (err) => {
168
+ log(`Erro no HTTP server: ${(err as Error).message}`);
169
+ });
170
+
171
+ http.listen(HTTP_PORT, '127.0.0.1', () => {
172
+ log(`Comando HTTP em http://localhost:${HTTP_PORT}/command`);
173
+ });
174
+ }
175
+
98
176
  // ── MCP tools ─────────────────────────────────────────────────────────────────
99
177
 
100
178
  const ColorEnum = z.enum([
@@ -172,41 +250,17 @@ server.tool(
172
250
  }
173
251
  );
174
252
 
175
- // ── HTTP command server (port 9242) ───────────────────────────────────────────
176
- // Accepts POST /command { type, payload } — lets any local client send operations
177
- // without needing an MCP session. Used by scripts and tests.
178
-
179
- function startCommandServer() {
180
- const http = createServer(async (req, res) => {
181
- if (req.method !== 'POST' || req.url !== '/command') {
182
- res.writeHead(404).end();
183
- return;
184
- }
185
- let body = '';
186
- req.on('data', (c: Buffer) => { body += c; });
187
- req.on('end', async () => {
188
- try {
189
- const { type, payload } = JSON.parse(body) as { type: string; payload: unknown };
190
- const result = await sendToPlugin(type, payload);
191
- res.writeHead(200, { 'Content-Type': 'application/json' });
192
- res.end(JSON.stringify({ success: true, data: result }));
193
- } catch (err) {
194
- res.writeHead(200, { 'Content-Type': 'application/json' });
195
- res.end(JSON.stringify({ success: false, error: (err as Error).message }));
196
- }
197
- });
198
- });
199
- http.listen(9242, '127.0.0.1', () => log('Comando HTTP em http://localhost:9242/command'));
200
- }
201
-
202
253
  // ── Boot ──────────────────────────────────────────────────────────────────────
203
254
 
204
- function log(msg: string) {
205
- process.stderr.write(`[IngresseFlow] ${msg}\n`);
255
+ const existing = await detectExistingBridge();
256
+ if (existing) {
257
+ mode = 'client';
258
+ log(`Bridge persistente detectada em :${HTTP_PORT} — operando em modo cliente.`);
259
+ } else {
260
+ mode = 'server';
261
+ await startWebSocketBridge();
262
+ startCommandServer();
206
263
  }
207
264
 
208
- await startBridge();
209
- startCommandServer();
210
-
211
265
  const transport = new StdioServerTransport();
212
266
  await server.connect(transport);