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 +21 -0
- package/README.md +108 -0
- package/extension-bridge/mcp-bridge.js +384 -0
- package/package.json +43 -0
- package/src/bridge/ws-server.js +159 -0
- package/src/index.js +304 -0
- package/src/queue/mcp-queue.js +162 -0
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();
|