iflow-mcp_orion4d-comfyui_mcp 1.0.0__tar.gz

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 (45) hide show
  1. iflow_mcp_orion4d_comfyui_mcp-1.0.0/.env +12 -0
  2. iflow_mcp_orion4d_comfyui_mcp-1.0.0/.env.exampleforGPT +30 -0
  3. iflow_mcp_orion4d_comfyui_mcp-1.0.0/.gitattributes +2 -0
  4. iflow_mcp_orion4d_comfyui_mcp-1.0.0/3017_process.log +3 -0
  5. iflow_mcp_orion4d_comfyui_mcp-1.0.0/Extension_Chrome/background.js +328 -0
  6. iflow_mcp_orion4d_comfyui_mcp-1.0.0/Extension_Chrome/icon128.png +0 -0
  7. iflow_mcp_orion4d_comfyui_mcp-1.0.0/Extension_Chrome/icon16.png +0 -0
  8. iflow_mcp_orion4d_comfyui_mcp-1.0.0/Extension_Chrome/icon48.png +0 -0
  9. iflow_mcp_orion4d_comfyui_mcp-1.0.0/Extension_Chrome/manifest.json +35 -0
  10. iflow_mcp_orion4d_comfyui_mcp-1.0.0/Extension_Chrome/popup.html +74 -0
  11. iflow_mcp_orion4d_comfyui_mcp-1.0.0/Extension_Chrome/popup.js +149 -0
  12. iflow_mcp_orion4d_comfyui_mcp-1.0.0/LICENSE +21 -0
  13. iflow_mcp_orion4d_comfyui_mcp-1.0.0/PKG-INFO +239 -0
  14. iflow_mcp_orion4d_comfyui_mcp-1.0.0/README.md +221 -0
  15. iflow_mcp_orion4d_comfyui_mcp-1.0.0/Script_bat/start_cloudflared.bat +31 -0
  16. iflow_mcp_orion4d_comfyui_mcp-1.0.0/Script_bat/start_mcp_server.bat +55 -0
  17. iflow_mcp_orion4d_comfyui_mcp-1.0.0/build/lib/iflow_mcp_orion4d_comfyui_mcp/__init__.py +9 -0
  18. iflow_mcp_orion4d_comfyui_mcp-1.0.0/build/lib/iflow_mcp_orion4d_comfyui_mcp/__main__.py +8 -0
  19. iflow_mcp_orion4d_comfyui_mcp-1.0.0/build/lib/iflow_mcp_orion4d_comfyui_mcp/browser_controller.py +123 -0
  20. iflow_mcp_orion4d_comfyui_mcp-1.0.0/build/lib/iflow_mcp_orion4d_comfyui_mcp/comfyui_client.py +301 -0
  21. iflow_mcp_orion4d_comfyui_mcp-1.0.0/build/lib/iflow_mcp_orion4d_comfyui_mcp/generate_key.py +158 -0
  22. iflow_mcp_orion4d_comfyui_mcp-1.0.0/build/lib/iflow_mcp_orion4d_comfyui_mcp/server.py +1027 -0
  23. iflow_mcp_orion4d_comfyui_mcp-1.0.0/docs/index.html +70 -0
  24. iflow_mcp_orion4d_comfyui_mcp-1.0.0/docs/security.html +256 -0
  25. iflow_mcp_orion4d_comfyui_mcp-1.0.0/docs/tuto_cloudflare_gpt.html +553 -0
  26. iflow_mcp_orion4d_comfyui_mcp-1.0.0/docs/tuto_gradio_google_cli.html +523 -0
  27. iflow_mcp_orion4d_comfyui_mcp-1.0.0/docs/tuto_gradio_ollama.html +458 -0
  28. iflow_mcp_orion4d_comfyui_mcp-1.0.0/iflow_mcp_orion4d_comfyui_mcp/__init__.py +9 -0
  29. iflow_mcp_orion4d_comfyui_mcp-1.0.0/iflow_mcp_orion4d_comfyui_mcp/__main__.py +8 -0
  30. iflow_mcp_orion4d_comfyui_mcp-1.0.0/iflow_mcp_orion4d_comfyui_mcp/browser_controller.py +123 -0
  31. iflow_mcp_orion4d_comfyui_mcp-1.0.0/iflow_mcp_orion4d_comfyui_mcp/comfyui_client.py +301 -0
  32. iflow_mcp_orion4d_comfyui_mcp-1.0.0/iflow_mcp_orion4d_comfyui_mcp/generate_key.py +158 -0
  33. iflow_mcp_orion4d_comfyui_mcp-1.0.0/iflow_mcp_orion4d_comfyui_mcp/server.py +1027 -0
  34. iflow_mcp_orion4d_comfyui_mcp-1.0.0/iflow_mcp_orion4d_comfyui_mcp.egg-info/PKG-INFO +241 -0
  35. iflow_mcp_orion4d_comfyui_mcp-1.0.0/iflow_mcp_orion4d_comfyui_mcp.egg-info/SOURCES.txt +16 -0
  36. iflow_mcp_orion4d_comfyui_mcp-1.0.0/iflow_mcp_orion4d_comfyui_mcp.egg-info/dependency_links.txt +1 -0
  37. iflow_mcp_orion4d_comfyui_mcp-1.0.0/iflow_mcp_orion4d_comfyui_mcp.egg-info/entry_points.txt +2 -0
  38. iflow_mcp_orion4d_comfyui_mcp-1.0.0/iflow_mcp_orion4d_comfyui_mcp.egg-info/requires.txt +10 -0
  39. iflow_mcp_orion4d_comfyui_mcp-1.0.0/iflow_mcp_orion4d_comfyui_mcp.egg-info/top_level.txt +1 -0
  40. iflow_mcp_orion4d_comfyui_mcp-1.0.0/language.json +1 -0
  41. iflow_mcp_orion4d_comfyui_mcp-1.0.0/package_name +1 -0
  42. iflow_mcp_orion4d_comfyui_mcp-1.0.0/push_info.json +5 -0
  43. iflow_mcp_orion4d_comfyui_mcp-1.0.0/pyproject.toml +25 -0
  44. iflow_mcp_orion4d_comfyui_mcp-1.0.0/requirements.txt +16 -0
  45. iflow_mcp_orion4d_comfyui_mcp-1.0.0/setup.py +33 -0
@@ -0,0 +1,12 @@
1
+ # MCP API Key for testing
2
+ MCP_API_KEY=test_key_123
3
+
4
+ # WebSocket Token for Chrome extension
5
+ WEBSOCKET_TOKEN=test_ws_token_123
6
+
7
+ # Browser control disabled for testing
8
+ ENABLE_BROWSER_CONTROL=false
9
+
10
+ # ComfyUI configuration (not needed for MCP protocol test)
11
+ # COMFYUI_BASE_URL=http://127.0.0.1:8188
12
+ # COMFYUI_ROOT=/path/to/comfyui
@@ -0,0 +1,30 @@
1
+ # ============================================================================
2
+ # Configuration Cloudflare Tunnel (avec Extension Chrome)
3
+ # ============================================================================
4
+
5
+ # ----- ComfyUI Local -----
6
+ COMFYUI_BASE_URL=http://127.0.0.1:8188
7
+ WORKFLOWS_DIR=workflows
8
+
9
+ # Chemin de votre comfyUI, Exemple D:\ComfyUI_dev\ComfyUI
10
+ COMFYUI_ROOT=
11
+
12
+ # ----- Sécurité API -----
13
+ # Décommente si tu veux activer l'authentification
14
+ # pour le moment les connecteurs ChatGPT personnalisés ne proposent pas d'API key
15
+ # Clé API à genérer (generate_key.py)
16
+ # MCP_API_KEY=ta_cle_api_forte_ici
17
+
18
+ # ----- Extension Chrome (via Cloudflare) -----
19
+ ENABLE_BROWSER_CONTROL=true
20
+
21
+ # Clé Websoket à genérer (generate_key.py)
22
+ WEBSOCKET_TOKEN=
23
+
24
+ # ----- Timeouts (augmentés pour Cloudflare) -----
25
+ HTTP_TIMEOUT=120
26
+ GENERATION_TIMEOUT=600
27
+
28
+ # Votre adresse URL tunnel sécurisé, attention garder cette adresse privée
29
+ # Toute personne avec l'URL peut accéder à votre serveur MCP
30
+ PUBLIC_URL=
@@ -0,0 +1,2 @@
1
+ # Auto detect text files and perform LF normalization
2
+ * text=auto
@@ -0,0 +1,3 @@
1
+ [2026-02-04 05:38:16] [SUCCESS] 步骤1-获取项目: 成功fork并克隆项目 orion4d/ComfyUI_mcp
2
+ [2026-02-04 05:38:55] [SUCCESS] 步骤2-阅读代码: 项目类型=Python, MCP服务端=是, 传输协议=stdio+HTTP
3
+ [2026-02-04 05:49:17] [SUCCESS] 步骤3-本地测试: 成功检测到28个工具, 无需额外环境变量
@@ -0,0 +1,328 @@
1
+ // =============================
2
+ // MCP ComfyUI Controller - background.js (MV3) - Version Corrigée
3
+ // =============================
4
+
5
+ // ---------- Configuration par défaut ----------
6
+ let MCP_WEBSOCKET_URL = "ws://127.0.0.1:8000/ws";
7
+ let WEBSOCKET_TOKEN = "";
8
+ let BROWSER_CONTROL_ENABLED = true;
9
+
10
+ // ---------- État WS ----------
11
+ let ws = null;
12
+ let reconnectTimeout = null;
13
+ let isConnecting = false;
14
+ let reconnectAttempts = 0;
15
+ const MAX_RECONNECT_ATTEMPTS = 5;
16
+
17
+ // ---------- Journal partagé (popup <-> bg) ----------
18
+ const LOG_BUFFER = [];
19
+ const LOG_MAX = 200;
20
+
21
+ function pushLog(entry) {
22
+ const stamp = new Date().toISOString().slice(11, 19); // HH:MM:SS
23
+ const row = { t: stamp, level: "info", ...entry };
24
+ LOG_BUFFER.push(row);
25
+ if (LOG_BUFFER.length > LOG_MAX) LOG_BUFFER.shift();
26
+
27
+ chrome.runtime.sendMessage({ action: "log_update", rows: [row] }, () => {
28
+ if (chrome.runtime.lastError) { /* Popup fermé, c'est normal */ }
29
+ });
30
+ }
31
+
32
+ // ---------- Chargement config depuis storage ----------
33
+ let CONFIG_READY = false;
34
+ const waitConfig = new Promise((resolve) => {
35
+ chrome.storage.sync.get(
36
+ ["mcpServerUrl", "mcpWebSocketToken", "mcpBrowserControlEnabled"],
37
+ (result) => {
38
+ if (result.mcpServerUrl) MCP_WEBSOCKET_URL = result.mcpServerUrl;
39
+ if (result.mcpWebSocketToken) WEBSOCKET_TOKEN = result.mcpWebSocketToken;
40
+ if (typeof result.mcpBrowserControlEnabled === "boolean") {
41
+ BROWSER_CONTROL_ENABLED = result.mcpBrowserControlEnabled;
42
+ }
43
+ pushLog({ level: "info", msg: "Config chargée" });
44
+ CONFIG_READY = true;
45
+ resolve();
46
+
47
+ if (BROWSER_CONTROL_ENABLED) {
48
+ pushLog({ level: "info", msg: "Connexion auto dans 3s…" });
49
+ setTimeout(connect, 3000);
50
+ } else {
51
+ pushLog({ level: "info", msg: "Contrôle navigateur désactivé" });
52
+ }
53
+ }
54
+ );
55
+ });
56
+
57
+ // ---------- Handlers messages (popup -> bg) ----------
58
+ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
59
+ // ... (Le reste de ce listener est parfait, pas de changement nécessaire) ...
60
+ if (message.action === "updateConfig") {
61
+ MCP_WEBSOCKET_URL = message.url ?? MCP_WEBSOCKET_URL;
62
+ WEBSOCKET_TOKEN = message.token ?? WEBSOCKET_TOKEN;
63
+ pushLog({ level: "info", msg: "Config mise à jour depuis popup" });
64
+ disconnect();
65
+ if (BROWSER_CONTROL_ENABLED) {
66
+ reconnectAttempts = 0;
67
+ connect();
68
+ }
69
+ sendResponse({ success: true });
70
+ return true;
71
+ }
72
+ if (message.action === "toggleBrowserControl") {
73
+ BROWSER_CONTROL_ENABLED = !!message.enabled;
74
+ pushLog({ level: "info", msg: `Contrôle ${BROWSER_CONTROL_ENABLED ? "activé" : "désactivé"}` });
75
+ if (BROWSER_CONTROL_ENABLED) {
76
+ reconnectAttempts = 0;
77
+ connect();
78
+ } else {
79
+ disconnect();
80
+ }
81
+ sendResponse({ success: true });
82
+ return true;
83
+ }
84
+ if (message.action === "reconnect") {
85
+ if (BROWSER_CONTROL_ENABLED) {
86
+ disconnect();
87
+ reconnectAttempts = 0;
88
+ connect();
89
+ }
90
+ sendResponse({ success: true });
91
+ return true;
92
+ }
93
+ if (message.action === "getConnectionStatus") {
94
+ sendResponse({
95
+ connected: ws && ws.readyState === WebSocket.OPEN,
96
+ connecting: isConnecting,
97
+ enabled: BROWSER_CONTROL_ENABLED,
98
+ });
99
+ return true;
100
+ }
101
+ if (message.action === "getLog") {
102
+ sendResponse({ rows: LOG_BUFFER });
103
+ return true;
104
+ }
105
+ if (message.action === "execAndReturn" && message.cmd) {
106
+ executeCommandAndReturn(message.cmd)
107
+ .then((data) => sendResponse({ ok: true, data }))
108
+ .catch((e) => sendResponse({ ok: false, error: String(e) }));
109
+ return true;
110
+ }
111
+ // Action non reconnue (implicite)
112
+ });
113
+
114
+ // ---------- Connexion WebSocket sécurisée ----------
115
+ async function connect() {
116
+ // ... (Cette fonction est parfaite, pas de changement nécessaire) ...
117
+ if (!CONFIG_READY) await waitConfig;
118
+ if (!BROWSER_CONTROL_ENABLED) {
119
+ pushLog({ level: "info", msg: "Connexion annulée (contrôle désactivé)" });
120
+ return;
121
+ }
122
+ if (!WEBSOCKET_TOKEN) {
123
+ pushLog({ level: "warn", msg: "Token absent → pas de tentative WS (évite 403)" });
124
+ return;
125
+ }
126
+ if (isConnecting || (ws && ws.readyState === WebSocket.OPEN)) return;
127
+ if (reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
128
+ pushLog({ level: "error", msg: "Max reconnexions atteint" });
129
+ reconnectAttempts = 0;
130
+ return;
131
+ }
132
+ isConnecting = true;
133
+ reconnectAttempts++;
134
+ const urlWithToken = `${MCP_WEBSOCKET_URL}?token=${encodeURIComponent(WEBSOCKET_TOKEN)}`;
135
+ pushLog({ level: "info", msg: `Connexion WS (${reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS})` });
136
+ try {
137
+ ws = new WebSocket(urlWithToken);
138
+ ws.onopen = () => {
139
+ isConnecting = false;
140
+ reconnectAttempts = 0;
141
+ pushLog({ level: "info", msg: "WS connecté" });
142
+ const pingInterval = setInterval(() => {
143
+ if (ws && ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ type: "ping" }));
144
+ else clearInterval(pingInterval);
145
+ }, 30000);
146
+ };
147
+ ws.onmessage = (event) => {
148
+ try {
149
+ const data = JSON.parse(event.data);
150
+ if (data.type === "pong") return;
151
+ if (data.error) {
152
+ pushLog({ level: "error", msg: `Erreur serveur: ${data.error}` });
153
+ if (data.message) pushLog({ level: "error", msg: data.message });
154
+ return;
155
+ }
156
+ pushLog({ level: "debug", msg: `Commande reçue: ${data.action || data.type || "—"}` });
157
+ executeCommand(data);
158
+ } catch (e) {
159
+ pushLog({ level: "error", msg: `JSON parse error: ${String(e)}` });
160
+ }
161
+ };
162
+ ws.onclose = (event) => {
163
+ isConnecting = false;
164
+ if (event.code === 1008) {
165
+ pushLog({ level: "error", msg: "Auth refusée (token invalide/manquant)" });
166
+ reconnectAttempts = MAX_RECONNECT_ATTEMPTS;
167
+ return;
168
+ }
169
+ pushLog({ level: "warn", msg: `WS fermé (${event.code})` });
170
+ if (BROWSER_CONTROL_ENABLED && reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
171
+ const delay = Math.min(5000 * reconnectAttempts, 30000);
172
+ pushLog({ level: "info", msg: `Reconnexion dans ${delay / 1000}s…` });
173
+ reconnectTimeout = setTimeout(connect, delay);
174
+ }
175
+ };
176
+ ws.onerror = () => {
177
+ isConnecting = false;
178
+ pushLog({ level: "warn", msg: "WS error (handshake/403 ?)" });
179
+ };
180
+ } catch (error) {
181
+ isConnecting = false;
182
+ pushLog({ level: "error", msg: `Création WS: ${String(error)}` });
183
+ if (BROWSER_CONTROL_ENABLED && reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
184
+ reconnectTimeout = setTimeout(connect, 5000);
185
+ }
186
+ }
187
+ }
188
+
189
+ function disconnect() {
190
+ // ... (Cette fonction est parfaite, pas de changement nécessaire) ...
191
+ if (reconnectTimeout) {
192
+ clearTimeout(reconnectTimeout);
193
+ reconnectTimeout = null;
194
+ }
195
+ if (ws) {
196
+ try { ws.close(); } catch {}
197
+ ws = null;
198
+ }
199
+ isConnecting = false;
200
+ pushLog({ level: "info", msg: "WS déconnecté" });
201
+ }
202
+
203
+ // ---------- Utilitaires ----------
204
+ function isValidSelector(selector) { /* ... parfait, pas de changement ... */ return true; }
205
+ async function getComfyTab() { /* ... parfait, pas de changement ... */ const tabs = await chrome.tabs.query({url: ["http://127.0.0.1:8188/*", "http://localhost:8188/*"]}); return tabs[0];}
206
+
207
+ // ---------- Exécution des commandes (reçues du WS) ----------
208
+ async function executeCommand(command) {
209
+ try {
210
+ const data = await executeCommandAndReturn(command);
211
+ if (ws && ws.readyState === WebSocket.OPEN) {
212
+ // CORRECTION : On construit la réponse pour le serveur MCP ici
213
+ // Le type de la réponse est dérivé de l'action de la commande
214
+ const responseType = {
215
+ "dump_dom": "dom_dump",
216
+ "get_nodes_map": "nodes_map",
217
+ "get_workflow": "workflow"
218
+ }[command.action];
219
+
220
+ if (responseType) {
221
+ // On envoie directement les données, sans le wrapper {ok: true}
222
+ ws.send(JSON.stringify({ type: responseType, data: data }));
223
+ pushLog({ level: "info", msg: `Réponse '${responseType}' envoyée au serveur` });
224
+ }
225
+ }
226
+ } catch (err) {
227
+ pushLog({ level: "error", msg: `Erreur exécution commande: ${String(err)}` });
228
+ if (ws && ws.readyState === WebSocket.OPEN) {
229
+ ws.send(JSON.stringify({ type: "error", message: `Erreur côté extension: ${String(err)}` }));
230
+ }
231
+ }
232
+ }
233
+
234
+
235
+ // ---------- Exécution de commande avec RETOUR de données (pour popup et WS) ----------
236
+ async function executeCommandAndReturn(command) {
237
+ const comfyTab = await getComfyTab();
238
+ if (!comfyTab) throw new Error("Aucun onglet ComfyUI (8188) trouvé");
239
+
240
+ const [res] = await chrome.scripting.executeScript({
241
+ target: { tabId: comfyTab.id },
242
+ world: 'MAIN',
243
+ func: injectedLogic, // AMÉLIORATION: On utilise une seule fonction d'injection
244
+ args: [command]
245
+ });
246
+
247
+ if (res && res.result) {
248
+ if (res.result.error) {
249
+ throw new Error(res.result.error + (res.result.message ? `: ${res.result.message}` : ''));
250
+ }
251
+ return res.result; // Retourne directement les données (ex: {count: ..., items: ...})
252
+ }
253
+ throw new Error("Aucun résultat du script injecté.");
254
+ }
255
+
256
+
257
+ // ========== Fonctions injectées dans la page ComfyUI ==========
258
+ // AMÉLIORATION: Toute la logique est maintenant dans une seule fonction pour la propreté.
259
+ function injectedLogic(command) {
260
+
261
+ // -- Fonctions utilitaires dans la page --
262
+ const waitForGraph = (ms = 5000) => new Promise((resolve) => {
263
+ const start = performance.now();
264
+ const tick = () => {
265
+ const g = (window.app && window.app.graph) || null;
266
+ if (g && (g._nodes || g.nodes) && typeof g.serialize === 'function') return resolve(g);
267
+ if (performance.now() - start > ms) return resolve(null);
268
+ requestAnimationFrame(tick);
269
+ };
270
+ tick();
271
+ });
272
+
273
+ // -- Logique principale --
274
+ switch (command.action) {
275
+ case "click": {
276
+ const el = document.querySelector(command.selector);
277
+ if (el) el.click();
278
+ return { ok: true };
279
+ }
280
+ case "fill": {
281
+ const el = document.querySelector(command.selector);
282
+ if (el) {
283
+ el.value = command.text ?? "";
284
+ el.dispatchEvent(new Event("input", { bubbles: true }));
285
+ el.dispatchEvent(new Event("change", { bubbles: true }));
286
+ }
287
+ return { ok: true };
288
+ }
289
+ case "dump_dom": {
290
+ // ... la fonction runInPageDump est déplacée ici ...
291
+ const maxItems = command.maxItems || 100;
292
+ const uniquePath = el => { /* ... (code de uniquePath inchangé) ... */ if(!el)return"";let path=[],n=5;while(el&&el.nodeType===1&&n-->0){let t=(e=>{if(!e)return"";if(e.id)return`#${e.id}`;let l=(e.className||"").toString().split(/\s+/).filter(Boolean).slice(0,3).join("."),r=(e.tagName||"div").toLowerCase();return l?`${r}.${l}`:r})(el);const o=el.parentElement;o&&Array.from(o.children).filter(t=>t.tagName===el.tagName).length>1&&(t+=`:nth-of-type(${Array.from(o.children).filter(t=>t.tagName===el.tagName).indexOf(el)+1})`),path.unshift(t),el=o}return path.join(" > ")};
293
+ const pick = (selector) => Array.from(document.querySelectorAll(selector));
294
+ const buttons = pick("button, [role='button'], .btn, .comfy-btn");
295
+ const inputs = pick("input, textarea, [contenteditable='true']");
296
+ const serialize = (el) => ({
297
+ tag: (el.tagName || "").toLowerCase(), id: el.id || null, classes: (el.className || "").toString().trim() || null,
298
+ selector: uniquePath(el), text: (el.innerText || el.value || "").trim().replace(/\s+/g, " ").slice(0, 120),
299
+ });
300
+ const items = [...buttons, ...inputs].slice(0, maxItems).map(serialize);
301
+ return { url: location.href, count: items.length, items };
302
+ }
303
+ case "get_nodes_map": {
304
+ return waitForGraph().then(g => {
305
+ if (!g) return { error: "graph_not_ready" };
306
+ const list = (g._nodes || g.nodes || []);
307
+ const nodes = Array.from(list).map(n => ({
308
+ id: n.id, type: n.type || n.constructor?.name || "Unknown",
309
+ title: n.title || n.type || `Node ${n.id}`, pos: n.pos || null
310
+ }));
311
+ return { url: location.href, count: nodes.length, nodes };
312
+ });
313
+ }
314
+ case "get_workflow": {
315
+ return waitForGraph().then(g => {
316
+ if (!g) return { error: "graph_not_ready" };
317
+ try {
318
+ const wf = g.serialize();
319
+ return { url: location.href, node_count: (wf.nodes || []).length, workflow: wf };
320
+ } catch (e) {
321
+ return { error: "serialize_failed", message: String(e) };
322
+ }
323
+ });
324
+ }
325
+ default:
326
+ return { error: `Action inconnue: ${command.action}` };
327
+ }
328
+ }
@@ -0,0 +1,35 @@
1
+ {
2
+ "manifest_version": 3,
3
+ "name": "MCP ComfyUI Controller",
4
+ "version": "1.0",
5
+ "description": "Connecte ComfyUI au serveur MCP local via WebSocket sécurisé.",
6
+ "minimum_chrome_version": "116",
7
+ "permissions": [
8
+ "scripting",
9
+ "tabs",
10
+ "storage"
11
+ ],
12
+ "host_permissions": [
13
+ "http://127.0.0.1:8188/*",
14
+ "http://localhost:8188/*"
15
+ ],
16
+ "content_security_policy": {
17
+ "extension_pages": "script-src 'self'; object-src 'self'; connect-src ws://127.0.0.1:* ws://localhost:* wss://*.trycloudflare.com wss://*.ngrok.io wss://*.loca.lt;"
18
+ },
19
+ "background": {
20
+ "service_worker": "background.js"
21
+ },
22
+ "action": {
23
+ "default_popup": "popup.html",
24
+ "default_icon": {
25
+ "16": "icon16.png",
26
+ "48": "icon48.png",
27
+ "128": "icon128.png"
28
+ }
29
+ },
30
+ "icons": {
31
+ "16": "icon16.png",
32
+ "48": "icon48.png",
33
+ "128": "icon128.png"
34
+ }
35
+ }
@@ -0,0 +1,74 @@
1
+ <!DOCTYPE html>
2
+ <html lang="fr">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>MCP ComfyUI Controller</title>
6
+ <style>
7
+ * { margin: 0; padding: 0; box-sizing: border-box; }
8
+ body { width: 500px; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: #fff; padding: 16px; }
9
+ h2 { font-size: 18px; margin-bottom: 12px; text-align: center; }
10
+ h3 { font-size: 14px; margin: 12px 0 8px; border-bottom: 1px solid rgba(255,255,255,0.3); padding-bottom: 4px; }
11
+ .status { display: flex; align-items: center; gap: 12px; padding: 12px; background: rgba(255,255,255,0.1); border-radius: 8px; margin-bottom: 16px; }
12
+ .status-indicator { width: 12px; height: 12px; border-radius: 50%; }
13
+ .status.connected .status-indicator { background: #4ade80; box-shadow: 0 0 8px #4ade80; }
14
+ .status.connecting .status-indicator { background: #fbbf24; animation: pulse 1.5s infinite; }
15
+ .status.disconnected .status-indicator { background: #f87171; }
16
+ @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }
17
+ .toggle-btn { width: 60px; height: 30px; background: rgba(255,255,255,0.2); border-radius: 15px; cursor: pointer; position: relative; transition: background 0.3s; }
18
+ .toggle-btn::after { content: ""; position: absolute; width: 26px; height: 26px; border-radius: 50%; background: #fff; top: 2px; left: 2px; transition: left 0.3s; }
19
+ .toggle-btn.active { background: #4ade80; }
20
+ .toggle-btn.active::after { left: 32px; }
21
+ .config-panel { background: rgba(255,255,255,0.15); padding: 12px; border-radius: 8px; margin-bottom: 12px; }
22
+ input[type="text"], input[type="password"] { width: 100%; padding: 8px; margin-top: 6px; border: none; border-radius: 4px; background: rgba(255,255,255,0.9); color: #333; }
23
+ button { padding: 8px 16px; margin: 4px; border: none; border-radius: 6px; cursor: pointer; font-weight: 600; transition: all 0.2s; }
24
+ .btn-primary { background: #4ade80; color: #000; }
25
+ .btn-primary:hover { background: #22c55e; transform: translateY(-2px); }
26
+ .btn-secondary { background: rgba(255,255,255,0.2); color: #fff; }
27
+ .btn-secondary:hover { background: rgba(255,255,255,0.3); }
28
+ textarea { width: 100%; height: 300px; background: #1a1a2e; color: #eee; padding: 10px; border: none; border-radius: 6px; font-family: 'Consolas', 'Monaco', monospace; font-size: 11px; resize: vertical; }
29
+ .btn-group { display: flex; gap: 8px; margin-top: 8px; flex-wrap: wrap; }
30
+ </style>
31
+ </head>
32
+ <body>
33
+ <h2>🎨 MCP ComfyUI Controller</h2>
34
+
35
+ <div class="status" id="status">
36
+ <div class="status-indicator"></div>
37
+ <span id="statusText">Déconnecté du serveur MCP</span>
38
+ </div>
39
+
40
+ <div style="display: flex; align-items: center; gap: 12px; margin-bottom: 12px;">
41
+ <span>Contrôle du navigateur</span>
42
+ <div class="toggle-btn" id="enableToggle"></div>
43
+ </div>
44
+
45
+ <div class="config-panel" id="configPanel">
46
+ <label>🔗 URL du serveur WebSocket</label>
47
+ <input type="text" id="serverUrl" placeholder="ws://127.0.0.1:8000/ws">
48
+
49
+ <label style="display: block; margin-top: 12px;">🔑 Token d'authentification</label>
50
+ <input type="password" id="authToken" placeholder="Optionnel">
51
+
52
+ <div style="margin-top: 12px;">
53
+ <button class="btn-primary" id="saveBtn">💾 Sauvegarder</button>
54
+ <button class="btn-secondary" id="testBtn">🔌 Tester</button>
55
+ </div>
56
+ </div>
57
+
58
+ <h3>📦 Sortie JSON</h3>
59
+ <textarea id="jsonOutput" placeholder="Les résultats JSON apparaîtront ici..."></textarea>
60
+
61
+ <div class="btn-group">
62
+ <button class="btn-primary" id="dumpDomBtn">🔍 Inspecter DOM</button>
63
+ <button class="btn-primary" id="nodesMapBtn">🧩 Nodes Map</button>
64
+ <button class="btn-primary" id="workflowBtn">📄 Workflow JSON</button>
65
+ <button class="btn-secondary" id="downloadJsonBtn">💾 Télécharger</button>
66
+ </div>
67
+
68
+ <div style="margin-top: 16px; text-align: center; font-size: 11px; opacity: 0.7;">
69
+ Développé pour ComfyUI • Connexion sécurisée par token
70
+ </div>
71
+
72
+ <script src="popup.js"></script>
73
+ </body>
74
+ </html>
@@ -0,0 +1,149 @@
1
+ // =============================
2
+ // MCP ComfyUI Controller - popup.js
3
+ // =============================
4
+
5
+ const $ = (id) => document.getElementById(id);
6
+ const statusEl = $("status");
7
+ const statusText = $("statusText");
8
+ const enableToggle = $("enableToggle");
9
+ const configPanel = $("configPanel");
10
+ const serverUrl = $("serverUrl");
11
+ const authToken = $("authToken");
12
+ const jsonOutput = $("jsonOutput");
13
+
14
+ function setStatus({ connected, connecting }) {
15
+ statusEl.classList.remove("connected", "connecting", "disconnected");
16
+ if (connected) {
17
+ statusEl.classList.add("connected");
18
+ statusText.textContent = "Connecté au serveur MCP";
19
+ } else if (connecting) {
20
+ statusEl.classList.add("connecting");
21
+ statusText.textContent = "Connexion au serveur MCP…";
22
+ } else {
23
+ statusEl.classList.add("disconnected");
24
+ statusText.textContent = "Déconnecté du serveur MCP";
25
+ }
26
+ }
27
+
28
+ function setToggle(enabled) {
29
+ enableToggle.classList.toggle("active", enabled);
30
+ configPanel.style.display = enabled ? "block" : "none";
31
+ }
32
+
33
+ function initPopup() {
34
+ chrome.storage.sync.get(
35
+ ["mcpServerUrl", "mcpWebSocketToken", "mcpBrowserControlEnabled"],
36
+ (res) => {
37
+ serverUrl.value = res.mcpServerUrl || "ws://127.0.0.1:8000/ws";
38
+ authToken.value = res.mcpWebSocketToken || "";
39
+ const enabled = typeof res.mcpBrowserControlEnabled === "boolean" ? res.mcpBrowserControlEnabled : true;
40
+ setToggle(enabled);
41
+
42
+ chrome.runtime.sendMessage({ action: "getConnectionStatus" }, (st) => {
43
+ if (!chrome.runtime.lastError) {
44
+ setStatus(st || { connected: false, connecting: false });
45
+ }
46
+ });
47
+ }
48
+ );
49
+
50
+ // Toggle activation
51
+ enableToggle.addEventListener("click", () => {
52
+ const enabled = !enableToggle.classList.contains("active");
53
+ setToggle(enabled);
54
+ chrome.storage.sync.set({ mcpBrowserControlEnabled: enabled }, () => {
55
+ chrome.runtime.sendMessage({ action: "toggleBrowserControl", enabled });
56
+ });
57
+ });
58
+
59
+ // Sauvegarder la configuration
60
+ $("saveBtn").addEventListener("click", () => {
61
+ const url = serverUrl.value.trim();
62
+ const token = authToken.value.trim();
63
+ chrome.storage.sync.set({ mcpServerUrl: url, mcpWebSocketToken: token }, () => {
64
+ chrome.runtime.sendMessage({ action: "updateConfig", url, token });
65
+ });
66
+ });
67
+
68
+ // Test de connexion
69
+ $("testBtn").addEventListener("click", () => {
70
+ chrome.runtime.sendMessage({ action: "getConnectionStatus" }, (st) => {
71
+ if (!chrome.runtime.lastError) {
72
+ setStatus(st || { connected: false, connecting: false });
73
+ }
74
+ });
75
+ });
76
+
77
+ // Inspecter DOM
78
+ $("dumpDomBtn").addEventListener("click", () => {
79
+ chrome.runtime.sendMessage(
80
+ { action: "execAndReturn", cmd: { action: "dump_dom", maxItems: 200 } },
81
+ (resp) => {
82
+ if (chrome.runtime.lastError) {
83
+ jsonOutput.value = `Erreur: ${chrome.runtime.lastError.message}`;
84
+ return;
85
+ }
86
+ jsonOutput.value = resp?.ok ? JSON.stringify(resp.data, null, 2) : `Erreur: ${resp?.error || "inconnue"}`;
87
+ jsonOutput.scrollTop = 0;
88
+ }
89
+ );
90
+ });
91
+
92
+ // Nodes Map
93
+ $("nodesMapBtn").addEventListener("click", () => {
94
+ chrome.runtime.sendMessage({ action: "execAndReturn", cmd: { action: "get_nodes_map" } }, (resp) => {
95
+ if (chrome.runtime.lastError) {
96
+ jsonOutput.value = `Erreur: ${chrome.runtime.lastError.message}`;
97
+ return;
98
+ }
99
+ jsonOutput.value = resp?.ok ? JSON.stringify(resp.data, null, 2) : `Erreur: ${resp?.error || "inconnue"}`;
100
+ jsonOutput.scrollTop = 0;
101
+ });
102
+ });
103
+
104
+ // Workflow JSON
105
+ $("workflowBtn").addEventListener("click", () => {
106
+ chrome.runtime.sendMessage({ action: "execAndReturn", cmd: { action: "get_workflow" } }, (resp) => {
107
+ if (chrome.runtime.lastError) {
108
+ jsonOutput.value = `Erreur: ${chrome.runtime.lastError.message}`;
109
+ return;
110
+ }
111
+ jsonOutput.value = resp?.ok ? JSON.stringify(resp.data, null, 2) : `Erreur: ${resp?.error || "inconnue"}`;
112
+ jsonOutput.scrollTop = 0;
113
+ });
114
+ });
115
+
116
+ // Télécharger JSON
117
+ $("downloadJsonBtn").addEventListener("click", () => {
118
+ const txt = jsonOutput.value || "";
119
+ if (!txt.trim()) return;
120
+ const blob = new Blob([txt], { type: "application/json;charset=utf-8" });
121
+ const url = URL.createObjectURL(blob);
122
+ const a = document.createElement("a");
123
+ a.href = url;
124
+ const now = new Date().toISOString().replace(/[:.]/g, "-");
125
+ a.download = `mcp-comfyui-${now}.json`;
126
+ document.body.appendChild(a);
127
+ a.click();
128
+ setTimeout(() => {
129
+ document.body.removeChild(a);
130
+ URL.revokeObjectURL(url);
131
+ }, 100);
132
+ });
133
+
134
+ // Rafraîchissement du statut de connexion toutes les 5 secondes
135
+ setInterval(() => {
136
+ chrome.runtime.sendMessage({ action: "getConnectionStatus" }, (st) => {
137
+ if (!chrome.runtime.lastError && st) {
138
+ setStatus(st);
139
+ }
140
+ });
141
+ }, 5000);
142
+ }
143
+
144
+ // Initialisation
145
+ if (document.readyState === "loading") {
146
+ document.addEventListener("DOMContentLoaded", initPopup);
147
+ } else {
148
+ initPopup();
149
+ }
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Philippe Joye (Orion4D)
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.