autoflow-mcp 2.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Autoflow (memrrizwan@gmail.com)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,108 @@
1
+ # โšก Autoflow MCP
2
+
3
+ **Zero-Config AI Agent Automation for Google Flow via the Autoflow Chrome Extension.**
4
+
5
+ Control [Google Flow](https://flow.google.com/) โ€” image & video generation with **Veo 3.1**, **Nano Banana**, and **Imagen 4** โ€” directly from your AI agent without browser flags, terminal profiles, or bot blocks.
6
+
7
+ ---
8
+
9
+ ## ๐ŸŒŸ Why Autoflow MCP?
10
+
11
+ Traditional browser MCP servers require launching Chrome with `--remote-debugging-port=9222`, hunting down profile directories, closing personal browser windows, and dealing with Google bot detection.
12
+
13
+ **Autoflow MCP eliminates all of that:**
14
+ * **Zero Configuration**: No terminal flags or profile paths. It bridges directly to your everyday Chrome browser.
15
+ * **100% Anti-Bot Immune**: Actions run inside your real, active Google session via the Autoflow extension.
16
+ * **Cross-Platform**: Works identically on Windows, macOS, and Linux out of the box.
17
+ * **Fast & Lightweight**: Zero headless browser overhead (< 15MB RAM).
18
+
19
+ ---
20
+
21
+ ## ๐Ÿ—๏ธ Architecture
22
+
23
+ ```
24
+ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
25
+ โ”‚ AI Client (Claude / OpenCode) โ”‚
26
+ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
27
+ โ”‚ Standard MCP Stdio
28
+ โ–ผ
29
+ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
30
+ โ”‚ Autoflow MCP Server โ”‚
31
+ โ”‚ (ws://127.0.0.1:19845) โ”‚
32
+ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
33
+ โ”‚ Local WebSocket RPC
34
+ โ–ผ
35
+ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
36
+ โ”‚ Autoflow Chrome Extension โ”‚
37
+ โ”‚ (Background Service Worker) โ”‚
38
+ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
39
+ โ”‚ Native DOM & CDP Automation
40
+ โ–ผ
41
+ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
42
+ โ”‚ Google Flow Web Studio โ”‚
43
+ โ”‚ (https://flow.google.com/) โ”‚
44
+ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
45
+ ```
46
+
47
+ ---
48
+
49
+ ## ๐Ÿš€ Quick Start
50
+
51
+ ### 1. Requirements
52
+ * Google Chrome with the **Autoflow Extension** installed and enabled.
53
+ * Node.js >= 18.
54
+
55
+ ### 2. Configure Your AI Agent
56
+
57
+ Add **Autoflow MCP** to your AI client's configuration (Claude Desktop, OpenCode, Antigravity `mcp_config.json`, or Cursor):
58
+
59
+ #### Using `npx` (Recommended for Users)
60
+ ```json
61
+ {
62
+ "mcpServers": {
63
+ "autoflow": {
64
+ "command": "npx",
65
+ "args": ["-y", "autoflow-mcp"]
66
+ }
67
+ }
68
+ }
69
+ ```
70
+
71
+ #### Running from Local Repository
72
+ ```json
73
+ {
74
+ "mcpServers": {
75
+ "autoflow": {
76
+ "command": "node",
77
+ "args": ["F:/google-flow-browser-mcp-main/src/index.js"]
78
+ }
79
+ }
80
+ }
81
+ ```
82
+
83
+ ### 3. Start Generating!
84
+ Just talk to your AI assistant:
85
+ * *"Check if Autoflow and Google Flow are connected."*
86
+ * *"Generate a cinematic drone shot of a neon cyberpunk city in 16:9 ratio using Veo 3.1."*
87
+ * *"List the available characters in my current Flow project."*
88
+
89
+ ---
90
+
91
+ ## ๐Ÿ”ง Tools Included
92
+
93
+ | Tool | Parameters | Description |
94
+ | :--- | :--- | :--- |
95
+ | `autoflow_status` | *(none)* | Checks extension and Google Flow tab connectivity. |
96
+ | `autoflow_open_flow` | *(none)* | Opens `https://flow.google.com/` in an active tab. |
97
+ | `autoflow_generate` | `prompt`, `mode`, `model`, `aspectRatio`, `outputCount`, `videoOption`, `characters` | Generates images or videos with Veo 3 or Nano Banana models. |
98
+ | `autoflow_list_characters` | *(none)* | Scans all available `@` character tags in the open project. |
99
+ | `autoflow_cancel` | `groupId` *(optional)* | Cancels any ongoing generation queue. |
100
+
101
+ ---
102
+
103
+ ## ๐Ÿงช Testing
104
+
105
+ Run the automated bridge test suite:
106
+ ```bash
107
+ npm test
108
+ ```
@@ -0,0 +1,384 @@
1
+ // Autoflow MCP WebSocket Bridge
2
+ // Connects the extension's background service worker to the local Autoflow MCP server
3
+
4
+ const MCP_WS_PORT = 19845;
5
+ const MCP_WS_URL = `ws://127.0.0.1:${MCP_WS_PORT}`;
6
+ const RECONNECT_DELAY_MS = 5000;
7
+
8
+ let socket = null;
9
+ let isConnected = false;
10
+ let reconnectTimer = null;
11
+ let activeJobs = new Map(); // requestId -> { resolve, reject, groupId }
12
+
13
+ function log(...args) {
14
+ console.log('[Autoflow MCP Bridge]', ...args);
15
+ }
16
+
17
+ function warn(...args) {
18
+ console.warn('[Autoflow MCP Bridge]', ...args);
19
+ }
20
+
21
+ function error(...args) {
22
+ console.error('[Autoflow MCP Bridge]', ...args);
23
+ }
24
+
25
+ // Find existing Google Flow tab or return null
26
+ async function getFlowTab() {
27
+ try {
28
+ const tabs = await chrome.tabs.query({
29
+ url: ['*://flow.google.com/*', '*://labs.google/*']
30
+ });
31
+ const tab = tabs.find(t => t.url && (t.url.includes('flow.google.com') || t.url.includes('labs.google'))) || null;
32
+ if (tab && tab.id) {
33
+ // Memory-Saver Shield: prevent Chrome from freezing/discarding this tab
34
+ try {
35
+ chrome.tabs.update(tab.id, { autoDiscardable: false });
36
+ } catch (e) {}
37
+ }
38
+ return tab;
39
+ } catch (err) {
40
+ warn('Error querying flow tabs:', err);
41
+ return null;
42
+ }
43
+ }
44
+
45
+ // Ensure Flow tab is open, protected from memory discarding, and ready
46
+ async function ensureFlowTab(options = {}) {
47
+ const { makeActive = false } = options;
48
+ let tab = await getFlowTab();
49
+
50
+ if (tab && tab.id) {
51
+ // Check if the tab's window is minimized. If minimized, un-minimize to normal state without stealing focus
52
+ try {
53
+ const win = await chrome.windows.get(tab.windowId);
54
+ if (win.state === 'minimized') {
55
+ await chrome.windows.update(tab.windowId, { state: 'normal', focused: false });
56
+ }
57
+ } catch (e) {}
58
+
59
+ // Ensure tab cannot be discarded by Chrome Memory Saver
60
+ try {
61
+ await chrome.tabs.update(tab.id, { autoDiscardable: false, active: makeActive });
62
+ } catch (e) {}
63
+
64
+ return tab;
65
+ }
66
+
67
+ log('No active Flow tab found, opening https://flow.google.com/ in background...');
68
+ tab = await chrome.tabs.create({
69
+ url: 'https://flow.google.com/',
70
+ active: makeActive
71
+ });
72
+
73
+ // Protect from discard immediately
74
+ try {
75
+ chrome.tabs.update(tab.id, { autoDiscardable: false });
76
+ } catch (e) {}
77
+
78
+ // Wait up to 15 seconds for navigation
79
+ for (let i = 0; i < 30; i++) {
80
+ await new Promise(r => setTimeout(r, 500));
81
+ try {
82
+ const current = await chrome.tabs.get(tab.id);
83
+ if (current && current.status === 'complete') {
84
+ await new Promise(r => setTimeout(r, 1000));
85
+ return current;
86
+ }
87
+ } catch (e) {}
88
+ }
89
+ return tab;
90
+ }
91
+
92
+ // Verify content script readiness before dispatching commands
93
+ async function waitForContentScript(tabId, maxWaitMs = 5000) {
94
+ const startTime = Date.now();
95
+ while (Date.now() - startTime < maxWaitMs) {
96
+ try {
97
+ const response = await new Promise((resolve) => {
98
+ chrome.tabs.sendMessage(tabId, { type: 'CHECK_FLOW_PAGE' }, (res) => {
99
+ if (chrome.runtime.lastError) resolve(null);
100
+ else resolve(res);
101
+ });
102
+ });
103
+ if (response && response.isFlowPage) {
104
+ return true;
105
+ }
106
+ } catch (e) {}
107
+ await new Promise(r => setTimeout(r, 500));
108
+ }
109
+ return false;
110
+ }
111
+
112
+ // Send message to local MCP server
113
+ function sendToMcp(payload) {
114
+ if (socket && socket.readyState === WebSocket.OPEN) {
115
+ socket.send(JSON.stringify(payload));
116
+ }
117
+ }
118
+
119
+ // Handle RPC calls from MCP Server
120
+ async function handleRpcRequest(data) {
121
+ const { id, action, params = {} } = data;
122
+ if (!id || !action) return;
123
+
124
+ log(`Received RPC action: ${action} (id: ${id})`, params);
125
+
126
+ try {
127
+ switch (action) {
128
+ case 'STATUS': {
129
+ const flowTab = await getFlowTab();
130
+ sendToMcp({
131
+ id,
132
+ success: true,
133
+ connected: true,
134
+ flowTabActive: !!flowTab,
135
+ flowTabUrl: flowTab ? flowTab.url : null,
136
+ version: chrome.runtime.getManifest().version
137
+ });
138
+ break;
139
+ }
140
+
141
+ case 'OPEN_FLOW': {
142
+ const tab = await ensureFlowTab();
143
+ sendToMcp({
144
+ id,
145
+ success: true,
146
+ tabId: tab.id,
147
+ url: tab.url
148
+ });
149
+ break;
150
+ }
151
+
152
+ case 'SCAN_CHARACTERS': {
153
+ const tab = await ensureFlowTab();
154
+ chrome.tabs.sendMessage(tab.id, { type: 'SCAN_CHARACTERS' }, (response) => {
155
+ const err = chrome.runtime.lastError;
156
+ if (err) {
157
+ sendToMcp({ id, success: false, error: err.message, characters: [] });
158
+ } else {
159
+ sendToMcp({ id, success: true, characters: response?.characters || [] });
160
+ }
161
+ });
162
+ break;
163
+ }
164
+
165
+ case 'CANCEL': {
166
+ const tab = await getFlowTab();
167
+ if (!tab || !tab.id) {
168
+ sendToMcp({ id, success: false, error: 'No Flow tab found to cancel' });
169
+ return;
170
+ }
171
+ chrome.tabs.sendMessage(tab.id, {
172
+ type: 'CANCEL_PROMPT_GROUP',
173
+ groupId: params.groupId
174
+ }, (response) => {
175
+ sendToMcp({ id, success: true, response });
176
+ });
177
+ break;
178
+ }
179
+
180
+ case 'GENERATE': {
181
+ const tab = await ensureFlowTab({ makeActive: true });
182
+ if (!tab || !tab.id) {
183
+ sendToMcp({ id, success: false, error: 'Could not open or find Google Flow tab' });
184
+ return;
185
+ }
186
+
187
+ // Ensure content script is mounted and active
188
+ await waitForContentScript(tab.id, 6000);
189
+
190
+ const groupId = `mcp-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
191
+ const payload = {
192
+ prompt: params.prompt || '',
193
+ mode: params.mode || (params.prompt?.toLowerCase().includes('video') ? 'textToVideo' : 'textToImage'),
194
+ model: params.model || (params.mode?.includes('Video') ? 'Veo 3.1 - Fast' : 'Nano Banana 2'),
195
+ aspectRatio: params.aspectRatio || '16:9',
196
+ outputCount: params.outputCount || 1,
197
+ videoOption: params.videoOption || '4s',
198
+ characters: params.characters || [],
199
+ images: params.images || [],
200
+ autoDownloadResourceQuality: params.autoDownloadResourceQuality || '1080',
201
+ folderName: params.folderName || 'Autoflow_MCP',
202
+ promptIndex: 0,
203
+ groupId: groupId,
204
+ isConcat: false
205
+ };
206
+
207
+ activeJobs.set(groupId, { id, startTime: Date.now() });
208
+
209
+ // Dispatch AUTO_FILL_FLOW to content script
210
+ chrome.tabs.sendMessage(tab.id, {
211
+ type: 'AUTO_FILL_FLOW',
212
+ payloads: [payload],
213
+ groupId: groupId,
214
+ concurrentPrompts: 1,
215
+ promptDelaySecondsMin: 0,
216
+ promptDelaySecondsMax: 0
217
+ }, (res) => {
218
+ const lastErr = chrome.runtime.lastError;
219
+ if (lastErr) {
220
+ warn(`Failed to dispatch AUTO_FILL_FLOW for job ${groupId}:`, lastErr);
221
+ activeJobs.delete(groupId);
222
+ sendToMcp({ id, success: false, error: lastErr.message });
223
+ } else {
224
+ log(`AUTO_FILL_FLOW dispatched successfully for job ${groupId}`);
225
+ }
226
+ });
227
+ break;
228
+ }
229
+
230
+ default:
231
+ sendToMcp({
232
+ id,
233
+ success: false,
234
+ error: `Unknown action: ${action}`
235
+ });
236
+ break;
237
+ }
238
+ } catch (err) {
239
+ error(`Error handling RPC action ${action}:`, err);
240
+ sendToMcp({
241
+ id,
242
+ success: false,
243
+ error: err.message || String(err)
244
+ });
245
+ }
246
+ }
247
+
248
+ // Forward progress and completion events from content script to MCP
249
+ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
250
+ if (!message || !message.type) return;
251
+
252
+ if (message.type === 'VIDEO_GENERATION_PROGRESS') {
253
+ const { data } = message;
254
+ if (data && data.groupId && activeJobs.has(data.groupId)) {
255
+ const job = activeJobs.get(data.groupId);
256
+ sendToMcp({
257
+ type: 'PROGRESS',
258
+ id: job.id,
259
+ percentage: data.percentage,
260
+ status: data.status,
261
+ prompt: data.prompt
262
+ });
263
+ }
264
+ } else if (message.type === 'PROMPT_GROUP_STATUS') {
265
+ const { data } = message;
266
+ if (data && data.id && activeJobs.has(data.id)) {
267
+ const job = activeJobs.get(data.id);
268
+ if (data.status === 'completed' || data.status === 'error' || data.status === 'cancelled') {
269
+ activeJobs.delete(data.id);
270
+ sendToMcp({
271
+ id: job.id,
272
+ success: data.status === 'completed',
273
+ status: data.status,
274
+ results: data.results,
275
+ totalCount: data.totalCount,
276
+ processedCount: data.processedCount
277
+ });
278
+ } else {
279
+ // In-progress status update
280
+ sendToMcp({
281
+ type: 'STATUS_UPDATE',
282
+ id: job.id,
283
+ status: data.status,
284
+ processedCount: data.processedCount,
285
+ totalCount: data.totalCount
286
+ });
287
+ }
288
+ }
289
+ } else if (message.type === 'GET_MCP_STATUS') {
290
+ sendResponse({ connected: isConnected, url: MCP_WS_URL, port: MCP_WS_PORT });
291
+ return true;
292
+ }
293
+ });
294
+
295
+ // Initialize WebSocket connection to local MCP server
296
+ export function initMcpBridge() {
297
+ if (socket && (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING)) {
298
+ return;
299
+ }
300
+
301
+ try {
302
+ socket = new WebSocket(MCP_WS_URL);
303
+
304
+ socket.onopen = () => {
305
+ isConnected = true;
306
+ log(`Connected to Autoflow MCP Server at ${MCP_WS_URL}`);
307
+ try {
308
+ chrome.storage.local.set({ mcp_connected: true, mcp_port: MCP_WS_PORT, mcp_last_seen: Date.now() });
309
+ } catch (e) {}
310
+
311
+ if (reconnectTimer) {
312
+ clearTimeout(reconnectTimer);
313
+ reconnectTimer = null;
314
+ }
315
+ sendToMcp({
316
+ type: 'EXTENSION_HELLO',
317
+ name: 'Autoflow Extension',
318
+ version: chrome.runtime.getManifest().version,
319
+ extensionId: chrome.runtime.id,
320
+ timestamp: Date.now()
321
+ });
322
+ };
323
+
324
+ socket.onmessage = (event) => {
325
+ try {
326
+ const data = JSON.parse(event.data);
327
+ handleRpcRequest(data);
328
+ } catch (err) {
329
+ warn('Invalid message received from MCP server:', event.data);
330
+ }
331
+ };
332
+
333
+ socket.onclose = () => {
334
+ if (isConnected) {
335
+ log('Disconnected from Autoflow MCP Server. Will attempt reconnect...');
336
+ }
337
+ isConnected = false;
338
+ try {
339
+ chrome.storage.local.set({ mcp_connected: false });
340
+ } catch (e) {}
341
+ scheduleReconnect();
342
+ };
343
+
344
+ socket.onerror = () => {
345
+ // Quietly handle connection errors (server may not be running yet)
346
+ isConnected = false;
347
+ try {
348
+ chrome.storage.local.set({ mcp_connected: false });
349
+ } catch (e) {}
350
+ };
351
+ } catch (err) {
352
+ scheduleReconnect();
353
+ }
354
+ }
355
+
356
+ function scheduleReconnect() {
357
+ if (reconnectTimer) return;
358
+ reconnectTimer = setTimeout(() => {
359
+ reconnectTimer = null;
360
+ initMcpBridge();
361
+ }, RECONNECT_DELAY_MS);
362
+ }
363
+
364
+ // Keep-alive alarm and tab triggers for Manifest V3 lifecycle
365
+ try {
366
+ chrome.alarms.create('autoflow_mcp_keepalive', { periodInMinutes: 1 });
367
+ chrome.alarms.onAlarm.addListener((alarm) => {
368
+ if (alarm.name === 'autoflow_mcp_keepalive') {
369
+ initMcpBridge();
370
+ }
371
+ });
372
+
373
+ chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
374
+ if (changeInfo.status === 'complete' && tab.url && (tab.url.includes('flow.google.com') || tab.url.includes('labs.google'))) {
375
+ initMcpBridge();
376
+ }
377
+ });
378
+ } catch (e) {
379
+ // Ignore in environments where alarms/tabs might be restricted
380
+ }
381
+
382
+ // Start bridge
383
+ initMcpBridge();
384
+
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "autoflow-mcp",
3
+ "version": "2.0.0",
4
+ "description": "Zero-config Model Context Protocol (MCP) server for Google Flow via Autoflow Chrome Extension",
5
+ "type": "module",
6
+ "main": "src/index.js",
7
+ "bin": {
8
+ "autoflow-mcp": "src/index.js"
9
+ },
10
+ "files": [
11
+ "src",
12
+ "extension-bridge",
13
+ "README.md",
14
+ "LICENSE"
15
+ ],
16
+ "scripts": {
17
+ "start": "node src/index.js",
18
+ "test": "node test-bridge.mjs && node test-queue.mjs"
19
+ },
20
+ "keywords": [
21
+ "mcp",
22
+ "model-context-protocol",
23
+ "google-flow",
24
+ "veo",
25
+ "veo-3",
26
+ "nano-banana",
27
+ "imagen",
28
+ "ai-agent",
29
+ "claude",
30
+ "opencode",
31
+ "chrome-extension"
32
+ ],
33
+ "author": "Rizwan <memrrizwan@gmail.com>",
34
+ "license": "MIT",
35
+ "dependencies": {
36
+ "@modelcontextprotocol/sdk": "^1.9.0",
37
+ "ws": "^8.18.0",
38
+ "zod": "^3.24.0"
39
+ },
40
+ "engines": {
41
+ "node": ">=18.0.0"
42
+ }
43
+ }
@@ -0,0 +1,159 @@
1
+ import { WebSocketServer, WebSocket } from 'ws';
2
+
3
+ const DEFAULT_PORT = 19845;
4
+ const DEFAULT_HOST = '127.0.0.1';
5
+ const DEFAULT_TIMEOUT_MS = 600000; // 10 minutes default for video generation
6
+
7
+ let wss = null;
8
+ let activeSocket = null;
9
+ let extensionInfo = null;
10
+ const pendingRequests = new Map();
11
+
12
+ export function startBridgeServer(port = process.env.AUTOFLOW_PORT || DEFAULT_PORT) {
13
+ if (wss) {
14
+ return { wss, port };
15
+ }
16
+
17
+ const numericPort = Number(port);
18
+
19
+ wss = new WebSocketServer({
20
+ port: numericPort,
21
+ host: DEFAULT_HOST
22
+ });
23
+
24
+ wss.on('listening', () => {
25
+ // Note: Use stderr for logging so stdout remains clean for MCP stdio protocol
26
+ console.error(`[Autoflow Bridge] WebSocket server listening on ws://${DEFAULT_HOST}:${numericPort}`);
27
+ });
28
+
29
+ wss.on('connection', (ws) => {
30
+ console.error('[Autoflow Bridge] Chrome Extension connected');
31
+ activeSocket = ws;
32
+
33
+ ws.on('message', (raw) => {
34
+ try {
35
+ const message = JSON.parse(raw.toString());
36
+ handleExtensionMessage(message);
37
+ } catch (err) {
38
+ console.error('[Autoflow Bridge] Failed to parse message from extension:', err);
39
+ }
40
+ });
41
+
42
+ ws.on('close', () => {
43
+ console.error('[Autoflow Bridge] Chrome Extension disconnected');
44
+ if (activeSocket === ws) {
45
+ activeSocket = null;
46
+ extensionInfo = null;
47
+ }
48
+ });
49
+
50
+ ws.on('error', (err) => {
51
+ console.error('[Autoflow Bridge] Socket error:', err.message);
52
+ });
53
+ });
54
+
55
+ wss.on('error', (err) => {
56
+ console.error('[Autoflow Bridge] Server error:', err.message);
57
+ });
58
+
59
+ return { wss, port: numericPort };
60
+ }
61
+
62
+ function handleExtensionMessage(message) {
63
+ if (!message) return;
64
+
65
+ // Extension Handshake
66
+ if (message.type === 'EXTENSION_HELLO') {
67
+ extensionInfo = {
68
+ name: message.name,
69
+ version: message.version,
70
+ extensionId: message.extensionId,
71
+ connectedAt: new Date().toISOString()
72
+ };
73
+ console.error(`[Autoflow Bridge] Extension identified: ${message.name} v${message.version}`);
74
+ return;
75
+ }
76
+
77
+ // Progress streaming
78
+ if (message.type === 'PROGRESS' || message.type === 'STATUS_UPDATE') {
79
+ const pending = pendingRequests.get(message.id);
80
+ if (pending && typeof pending.onProgress === 'function') {
81
+ pending.onProgress(message);
82
+ }
83
+ return;
84
+ }
85
+
86
+ // Response to an RPC call
87
+ if (message.id && pendingRequests.has(message.id)) {
88
+ const { resolve, reject, timeoutTimer } = pendingRequests.get(message.id);
89
+ clearTimeout(timeoutTimer);
90
+ pendingRequests.delete(message.id);
91
+
92
+ if (message.success) {
93
+ resolve(message);
94
+ } else {
95
+ reject(new Error(message.error || 'Unknown error occurred in extension'));
96
+ }
97
+ }
98
+ }
99
+
100
+ export function isExtensionConnected() {
101
+ return activeSocket !== null && activeSocket.readyState === WebSocket.OPEN;
102
+ }
103
+
104
+ export function getExtensionInfo() {
105
+ return {
106
+ connected: isExtensionConnected(),
107
+ info: extensionInfo
108
+ };
109
+ }
110
+
111
+ export function sendRpc(action, params = {}, options = {}) {
112
+ return new Promise((resolve, reject) => {
113
+ if (!isExtensionConnected()) {
114
+ return reject(new Error(
115
+ 'Autoflow Chrome Extension is not connected. Please ensure Google Chrome is open with the Autoflow extension active.'
116
+ ));
117
+ }
118
+
119
+ const id = `mcp_req_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`;
120
+ const timeoutMs = options.timeoutMs || DEFAULT_TIMEOUT_MS;
121
+
122
+ const timeoutTimer = setTimeout(() => {
123
+ if (pendingRequests.has(id)) {
124
+ pendingRequests.delete(id);
125
+ reject(new Error(`Autoflow request '${action}' timed out after ${timeoutMs / 1000}s`));
126
+ }
127
+ }, timeoutMs);
128
+
129
+ pendingRequests.set(id, {
130
+ resolve,
131
+ reject,
132
+ timeoutTimer,
133
+ onProgress: options.onProgress
134
+ });
135
+
136
+ const payload = JSON.stringify({
137
+ id,
138
+ action,
139
+ params
140
+ });
141
+
142
+ activeSocket.send(payload, (err) => {
143
+ if (err) {
144
+ clearTimeout(timeoutTimer);
145
+ pendingRequests.delete(id);
146
+ reject(new Error(`Failed to send command to extension: ${err.message}`));
147
+ }
148
+ });
149
+ });
150
+ }
151
+
152
+ export function closeBridgeServer() {
153
+ if (wss) {
154
+ wss.close();
155
+ wss = null;
156
+ activeSocket = null;
157
+ extensionInfo = null;
158
+ }
159
+ }
package/src/index.js ADDED
@@ -0,0 +1,304 @@
1
+ #!/usr/bin/env node
2
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
3
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
+ import {
5
+ CallToolRequestSchema,
6
+ ListToolsRequestSchema,
7
+ ErrorCode,
8
+ McpError
9
+ } from '@modelcontextprotocol/sdk/types.js';
10
+ import {
11
+ startBridgeServer,
12
+ isExtensionConnected,
13
+ getExtensionInfo,
14
+ sendRpc
15
+ } from './bridge/ws-server.js';
16
+ import { jobQueue } from './queue/mcp-queue.js';
17
+
18
+ // Start the local WebSocket Bridge for the Autoflow Chrome Extension
19
+ const port = process.env.AUTOFLOW_PORT || 19845;
20
+ startBridgeServer(port);
21
+
22
+ // Initialize MCP Server
23
+ const server = new Server(
24
+ {
25
+ name: 'autoflow-mcp',
26
+ version: '2.0.0'
27
+ },
28
+ {
29
+ capabilities: {
30
+ tools: {}
31
+ }
32
+ }
33
+ );
34
+
35
+ const TOOL_DEFINITIONS = [
36
+ {
37
+ name: 'autoflow_status',
38
+ description: 'Check the connection status of the Autoflow Chrome Extension and Google Flow tab.',
39
+ inputSchema: {
40
+ type: 'object',
41
+ properties: {}
42
+ }
43
+ },
44
+ {
45
+ name: 'autoflow_open_flow',
46
+ description: 'Instruct the Autoflow extension to open Google Flow (https://flow.google.com/) in an active browser tab.',
47
+ inputSchema: {
48
+ type: 'object',
49
+ properties: {}
50
+ }
51
+ },
52
+ {
53
+ name: 'autoflow_get_queue',
54
+ description: 'Inspect the current status of the Autoflow sequential job queue (active job, pending jobs, and recent history).',
55
+ inputSchema: {
56
+ type: 'object',
57
+ properties: {}
58
+ }
59
+ },
60
+ {
61
+ name: 'autoflow_generate',
62
+ description: 'Generate an AI video or image on Google Flow using Veo 3 or Nano Banana models via the Autoflow extension (queued sequentially).',
63
+ inputSchema: {
64
+ type: 'object',
65
+ properties: {
66
+ prompt: {
67
+ type: 'string',
68
+ description: 'The creative prompt for the image or video generation.'
69
+ },
70
+ mode: {
71
+ type: 'string',
72
+ description: 'Generation mode: textToVideo, imageToVideo, textToImage, imageToImage.',
73
+ enum: ['textToVideo', 'imageToVideo', 'textToImage', 'imageToImage']
74
+ },
75
+ model: {
76
+ type: 'string',
77
+ description: 'AI model to use (e.g. "Veo 3.1 - Fast", "Veo 3.1 - Quality", "Nano Banana 2", "Imagen 4").'
78
+ },
79
+ aspectRatio: {
80
+ type: 'string',
81
+ description: 'Aspect ratio: "16:9", "9:16", "1:1", "4:3", "3:4".',
82
+ enum: ['16:9', '9:16', '1:1', '4:3', '3:4']
83
+ },
84
+ outputCount: {
85
+ type: 'number',
86
+ description: 'Number of outputs to generate (1, 2, or 4). Default is 1.',
87
+ enum: [1, 2, 4]
88
+ },
89
+ videoOption: {
90
+ type: 'string',
91
+ description: 'Duration for video: "4s", "6s", "8s", "10s".',
92
+ enum: ['4s', '6s', '8s', '10s']
93
+ },
94
+ characters: {
95
+ type: 'array',
96
+ items: { type: 'string' },
97
+ description: 'List of character names to automatically mention with @ in the prompt.'
98
+ },
99
+ autoDownloadResourceQuality: {
100
+ type: 'string',
101
+ description: 'Download quality: "1080", "2k", "4k", or "no-download".',
102
+ enum: ['1080', '2k', '4k', 'no-download']
103
+ },
104
+ folderName: {
105
+ type: 'string',
106
+ description: 'Folder name where downloaded files should be saved.'
107
+ }
108
+ },
109
+ required: ['prompt']
110
+ }
111
+ },
112
+ {
113
+ name: 'autoflow_list_characters',
114
+ description: 'Scan and list all available character tags in the currently open Google Flow project.',
115
+ inputSchema: {
116
+ type: 'object',
117
+ properties: {}
118
+ }
119
+ },
120
+ {
121
+ name: 'autoflow_cancel',
122
+ description: 'Cancel any running or queued generation job in Google Flow.',
123
+ inputSchema: {
124
+ type: 'object',
125
+ properties: {
126
+ jobId: {
127
+ type: 'string',
128
+ description: 'Optional job ID to cancel. If omitted, cancels the currently active job.'
129
+ }
130
+ }
131
+ }
132
+ }
133
+ ];
134
+
135
+ // Register list of tools
136
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
137
+ return { tools: TOOL_DEFINITIONS };
138
+ });
139
+
140
+ // Handle tool execution requests
141
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
142
+ const { name, arguments: args = {} } = request.params;
143
+
144
+ try {
145
+ switch (name) {
146
+ case 'autoflow_status': {
147
+ if (!isExtensionConnected()) {
148
+ return {
149
+ content: [
150
+ {
151
+ type: 'text',
152
+ text: JSON.stringify(
153
+ {
154
+ status: 'disconnected',
155
+ extensionConnected: false,
156
+ message: 'Autoflow Chrome Extension is not connected. Please ensure Google Chrome is open with the Autoflow extension active.'
157
+ },
158
+ null,
159
+ 2
160
+ )
161
+ }
162
+ ]
163
+ };
164
+ }
165
+
166
+ const extStatus = await sendRpc('STATUS', {}, { timeoutMs: 10000 });
167
+ const info = getExtensionInfo();
168
+
169
+ return {
170
+ content: [
171
+ {
172
+ type: 'text',
173
+ text: JSON.stringify(
174
+ {
175
+ status: 'connected',
176
+ extensionConnected: true,
177
+ extension: info.info,
178
+ flow: extStatus,
179
+ queue: jobQueue.getQueueState()
180
+ },
181
+ null,
182
+ 2
183
+ )
184
+ }
185
+ ]
186
+ };
187
+ }
188
+
189
+ case 'autoflow_open_flow': {
190
+ const result = await sendRpc('OPEN_FLOW', {}, { timeoutMs: 15000 });
191
+ return {
192
+ content: [
193
+ {
194
+ type: 'text',
195
+ text: JSON.stringify(result, null, 2)
196
+ }
197
+ ]
198
+ };
199
+ }
200
+
201
+ case 'autoflow_get_queue': {
202
+ return {
203
+ content: [
204
+ {
205
+ type: 'text',
206
+ text: JSON.stringify(jobQueue.getQueueState(), null, 2)
207
+ }
208
+ ]
209
+ };
210
+ }
211
+
212
+ case 'autoflow_generate': {
213
+ const prompt = args.prompt;
214
+ const isVideo = args.mode?.includes('Video') ||
215
+ (!args.mode && (prompt.toLowerCase().includes('video') || prompt.toLowerCase().includes('shot') || prompt.toLowerCase().includes('cinematic')));
216
+
217
+ const defaultMode = isVideo ? 'textToVideo' : 'textToImage';
218
+ const defaultModel = isVideo ? 'Veo 3.1 - Fast' : 'Nano Banana 2';
219
+
220
+ const params = {
221
+ prompt,
222
+ mode: args.mode || defaultMode,
223
+ model: args.model || defaultModel,
224
+ aspectRatio: args.aspectRatio || '16:9',
225
+ outputCount: args.outputCount || 1,
226
+ videoOption: args.videoOption || '4s',
227
+ characters: args.characters || [],
228
+ autoDownloadResourceQuality: args.autoDownloadResourceQuality || '1080',
229
+ folderName: args.folderName || 'Autoflow_MCP'
230
+ };
231
+
232
+ // Route through the sequential FIFO queue to prevent collisions
233
+ const result = await jobQueue.enqueue(params);
234
+
235
+ return {
236
+ content: [
237
+ {
238
+ type: 'text',
239
+ text: JSON.stringify(result, null, 2)
240
+ }
241
+ ]
242
+ };
243
+ }
244
+
245
+ case 'autoflow_list_characters': {
246
+ const result = await sendRpc('SCAN_CHARACTERS', {}, { timeoutMs: 20000 });
247
+ return {
248
+ content: [
249
+ {
250
+ type: 'text',
251
+ text: JSON.stringify(result, null, 2)
252
+ }
253
+ ]
254
+ };
255
+ }
256
+
257
+ case 'autoflow_cancel': {
258
+ const result = await jobQueue.cancel(args.jobId || args.groupId);
259
+ return {
260
+ content: [
261
+ {
262
+ type: 'text',
263
+ text: JSON.stringify(result, null, 2)
264
+ }
265
+ ]
266
+ };
267
+ }
268
+
269
+ default:
270
+ throw new McpError(ErrorCode.MethodNotFound, `Tool '${name}' not found.`);
271
+ }
272
+ } catch (err) {
273
+ return {
274
+ content: [
275
+ {
276
+ type: 'text',
277
+ text: JSON.stringify(
278
+ {
279
+ error: err.message || String(err),
280
+ suggestion: !isExtensionConnected()
281
+ ? 'Open Google Chrome and verify the Autoflow extension is active.'
282
+ : 'Ensure a tab with https://flow.google.com/ is open.'
283
+ },
284
+ null,
285
+ 2
286
+ )
287
+ }
288
+ ],
289
+ isError: true
290
+ };
291
+ }
292
+ });
293
+
294
+ // Run MCP Stdio transport
295
+ async function run() {
296
+ const transport = new StdioServerTransport();
297
+ await server.connect(transport);
298
+ console.error('[Autoflow MCP] Server started and listening via stdio.');
299
+ }
300
+
301
+ run().catch((err) => {
302
+ console.error('[Autoflow MCP] Fatal error starting server:', err);
303
+ process.exit(1);
304
+ });
@@ -0,0 +1,162 @@
1
+ import { sendRpc, isExtensionConnected } from '../bridge/ws-server.js';
2
+
3
+ export class McpJobQueue {
4
+ constructor() {
5
+ this.queue = [];
6
+ this.activeJob = null;
7
+ this.history = [];
8
+ this.isProcessing = false;
9
+ }
10
+
11
+ // Enqueue a new generation job with strict sequential FIFO processing
12
+ enqueue(params) {
13
+ const id = `job_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;
14
+ const job = {
15
+ id,
16
+ prompt: params.prompt,
17
+ params,
18
+ status: 'queued',
19
+ progress: 0,
20
+ progressStatus: 'queued',
21
+ createdAt: new Date().toISOString(),
22
+ startedAt: null,
23
+ completedAt: null,
24
+ result: null,
25
+ error: null
26
+ };
27
+
28
+ const promise = new Promise((resolve, reject) => {
29
+ job.resolve = resolve;
30
+ job.reject = reject;
31
+ });
32
+
33
+ this.queue.push(job);
34
+ console.error(`[Autoflow Queue] Enqueued job ${id} ("${params.prompt.substring(0, 40)}..."). Queue depth: ${this.queue.length}`);
35
+ this.processNext();
36
+
37
+ return promise;
38
+ }
39
+
40
+ async processNext() {
41
+ if (this.isProcessing || this.queue.length === 0) {
42
+ return;
43
+ }
44
+
45
+ this.isProcessing = true;
46
+ this.activeJob = this.queue.shift();
47
+ this.activeJob.status = 'running';
48
+ this.activeJob.startedAt = new Date().toISOString();
49
+
50
+ console.error(`[Autoflow Queue] Processing job ${this.activeJob.id}: "${this.activeJob.prompt.substring(0, 40)}..."`);
51
+
52
+ try {
53
+ const result = await sendRpc('GENERATE', this.activeJob.params, {
54
+ timeoutMs: 600000, // 10 minute timeout
55
+ onProgress: (prog) => {
56
+ if (this.activeJob) {
57
+ this.activeJob.progress = prog.percentage || this.activeJob.progress;
58
+ this.activeJob.progressStatus = prog.status || 'generating';
59
+ }
60
+ }
61
+ });
62
+
63
+ this.activeJob.status = 'completed';
64
+ this.activeJob.completedAt = new Date().toISOString();
65
+ this.activeJob.progress = 100;
66
+ this.activeJob.result = result;
67
+
68
+ this.addToHistory(this.activeJob);
69
+ this.activeJob.resolve(result);
70
+ } catch (err) {
71
+ console.error(`[Autoflow Queue] Job ${this.activeJob.id} failed:`, err.message);
72
+ this.activeJob.status = 'failed';
73
+ this.activeJob.completedAt = new Date().toISOString();
74
+ this.activeJob.error = err.message;
75
+
76
+ this.addToHistory(this.activeJob);
77
+ this.activeJob.reject(err);
78
+ } finally {
79
+ this.activeJob = null;
80
+ this.isProcessing = false;
81
+ this.processNext();
82
+ }
83
+ }
84
+
85
+ // Cancel an active or pending job
86
+ async cancel(jobId) {
87
+ // If no jobId specified, cancel active job or first pending
88
+ if (!jobId && this.activeJob) {
89
+ jobId = this.activeJob.id;
90
+ }
91
+
92
+ if (this.activeJob && this.activeJob.id === jobId) {
93
+ console.error(`[Autoflow Queue] Cancelling active job ${jobId}`);
94
+ try {
95
+ await sendRpc('CANCEL', { groupId: this.activeJob.id });
96
+ } catch (e) {}
97
+
98
+ this.activeJob.status = 'cancelled';
99
+ this.activeJob.completedAt = new Date().toISOString();
100
+ this.activeJob.error = 'Cancelled by user';
101
+ this.addToHistory(this.activeJob);
102
+ this.activeJob.reject(new Error('Job was cancelled'));
103
+
104
+ this.activeJob = null;
105
+ this.isProcessing = false;
106
+ this.processNext();
107
+ return { success: true, message: `Active job ${jobId} cancelled` };
108
+ }
109
+
110
+ const index = this.queue.findIndex(j => j.id === jobId);
111
+ if (index !== -1) {
112
+ const [removed] = this.queue.splice(index, 1);
113
+ removed.status = 'cancelled';
114
+ removed.completedAt = new Date().toISOString();
115
+ removed.error = 'Cancelled before execution';
116
+ this.addToHistory(removed);
117
+ removed.reject(new Error('Job was cancelled from queue'));
118
+ return { success: true, message: `Queued job ${jobId} cancelled` };
119
+ }
120
+
121
+ return { success: false, message: `Job ${jobId || 'current'} not found` };
122
+ }
123
+
124
+ addToHistory(job) {
125
+ this.history.unshift({
126
+ id: job.id,
127
+ prompt: job.prompt,
128
+ status: job.status,
129
+ createdAt: job.createdAt,
130
+ startedAt: job.startedAt,
131
+ completedAt: job.completedAt,
132
+ progress: job.progress,
133
+ error: job.error
134
+ });
135
+
136
+ if (this.history.length > 20) {
137
+ this.history.pop();
138
+ }
139
+ }
140
+
141
+ getQueueState() {
142
+ return {
143
+ activeJob: this.activeJob ? {
144
+ id: this.activeJob.id,
145
+ prompt: this.activeJob.prompt,
146
+ status: this.activeJob.status,
147
+ progress: this.activeJob.progress,
148
+ progressStatus: this.activeJob.progressStatus,
149
+ startedAt: this.activeJob.startedAt
150
+ } : null,
151
+ queueLength: this.queue.length,
152
+ pendingJobs: this.queue.map(j => ({
153
+ id: j.id,
154
+ prompt: j.prompt,
155
+ createdAt: j.createdAt
156
+ })),
157
+ recentHistory: this.history.slice(0, 5)
158
+ };
159
+ }
160
+ }
161
+
162
+ export const jobQueue = new McpJobQueue();