#
tokens: 7476/50000 12/12 files
lines: on (toggle) GitHub
raw markdown copy reset
# Directory Structure

```
├── .dockerignore
├── .gitignore
├── .python-version
├── Dockerfile
├── Dockerfile.sse
├── LICENSE
├── pyproject.toml
├── README.md
├── requirements.txt
├── resources
│   └── run_workflow_from_file_demo.png
├── src
│   ├── .env
│   ├── client
│   │   ├── __init__.py
│   │   └── comfyui.py
│   ├── server.py
│   └── test_comfyui.py
├── uv.lock
└── workflows
    └── text_to_image.json
```

# Files

--------------------------------------------------------------------------------
/.python-version:
--------------------------------------------------------------------------------

```
1 | 3.12
2 | 
```

--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------

```
 1 | test.py
 2 | test
 3 | 
 4 | # Git
 5 | .git
 6 | .gitignore
 7 | .github
 8 | 
 9 | # Python
10 | __pycache__/
11 | *.py[cod]
12 | *$py.class
13 | *.so
14 | .Python
15 | env/
16 | build/
17 | develop-eggs/
18 | dist/
19 | downloads/
20 | eggs/
21 | .eggs/
22 | lib/
23 | lib64/
24 | parts/
25 | sdist/
26 | var/
27 | wheels/
28 | *.egg-info/
29 | .installed.cfg
30 | *.egg
31 | 
32 | # Virtual Environment
33 | venv/
34 | ENV/
35 | .env
36 | .venv
37 | 
38 | # IDE
39 | .idea/
40 | .vscode/
41 | *.swp
42 | *.swo
43 | .DS_Store
44 | 
45 | # Docker
46 | Dockerfile
47 | docker-compose.yml
48 | .docker/
49 | 
50 | # Logs
51 | *.log
52 | logs/
53 | 
54 | # Test
55 | test/
56 | tests/
57 | .coverage
58 | .pytest_cache/
59 | htmlcov/
60 | 
61 | # Documentation
62 | docs/
63 | *.md
64 | 
```

--------------------------------------------------------------------------------
/src/.env:
--------------------------------------------------------------------------------

```
 1 | # if true, the server will return the url of the generated image
 2 | # if false, the server will return the image bytes
 3 | RETURN_URL=true
 4 | 
 5 | # ComfyUI parameters
 6 | # When using Docker, it's recommended to set the host as 'host.docker.internal'
 7 | # But, if you don't set up a separate volume, downloading images with the download_image tool will be difficult
 8 | # COMFYUI_HOST=host.docker.internal
 9 | COMFYUI_HOST=localhost
10 | COMFYUI_PORT=8188
11 | # If you use authentication setting, you can set the authentication type and value
12 | # COMFYUI_AUTHENTICATION="Bearer <token>"
13 | # COMFYUI_AUTHENTICATION="Basic <b64_encoded_credentials>"
14 | 
15 | # The directory of the workflow files
16 | WORKFLOW_DIR=workflows
17 | 
18 | # MCP Transport
19 | # sse or stdio
20 | MCP_TRANSPORT=stdio
21 | 
```

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
  1 | test
  2 | 
  3 | # Byte-compiled / optimized / DLL files
  4 | __pycache__/
  5 | *.py[cod]
  6 | *$py.class
  7 | 
  8 | # C extensions
  9 | *.so
 10 | 
 11 | # Distribution / packaging
 12 | .Python
 13 | build/
 14 | develop-eggs/
 15 | dist/
 16 | downloads/
 17 | eggs/
 18 | .eggs/
 19 | lib/
 20 | lib64/
 21 | parts/
 22 | sdist/
 23 | var/
 24 | wheels/
 25 | share/python-wheels/
 26 | *.egg-info/
 27 | .installed.cfg
 28 | *.egg
 29 | MANIFEST
 30 | 
 31 | # PyInstaller
 32 | #  Usually these files are written by a python script from a template
 33 | #  before PyInstaller builds the exe, so as to inject date/other infos into it.
 34 | *.manifest
 35 | *.spec
 36 | 
 37 | # Installer logs
 38 | pip-log.txt
 39 | pip-delete-this-directory.txt
 40 | 
 41 | # Unit test / coverage reports
 42 | htmlcov/
 43 | .tox/
 44 | .nox/
 45 | .coverage
 46 | .coverage.*
 47 | .cache
 48 | nosetests.xml
 49 | coverage.xml
 50 | *.cover
 51 | *.py,cover
 52 | .hypothesis/
 53 | .pytest_cache/
 54 | cover/
 55 | 
 56 | # Translations
 57 | *.mo
 58 | *.pot
 59 | 
 60 | # Django stuff:
 61 | *.log
 62 | local_settings.py
 63 | db.sqlite3
 64 | db.sqlite3-journal
 65 | 
 66 | # Flask stuff:
 67 | instance/
 68 | .webassets-cache
 69 | 
 70 | # Scrapy stuff:
 71 | .scrapy
 72 | 
 73 | # Sphinx documentation
 74 | docs/_build/
 75 | 
 76 | # PyBuilder
 77 | .pybuilder/
 78 | target/
 79 | 
 80 | # Jupyter Notebook
 81 | .ipynb_checkpoints
 82 | 
 83 | # IPython
 84 | profile_default/
 85 | ipython_config.py
 86 | 
 87 | # pyenv
 88 | #   For a library or package, you might want to ignore these files since the code is
 89 | #   intended to run in multiple environments; otherwise, check them in:
 90 | # .python-version
 91 | 
 92 | # pipenv
 93 | #   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
 94 | #   However, in case of collaboration, if having platform-specific dependencies or dependencies
 95 | #   having no cross-platform support, pipenv may install dependencies that don't work, or not
 96 | #   install all needed dependencies.
 97 | #Pipfile.lock
 98 | 
 99 | # UV
100 | #   Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
101 | #   This is especially recommended for binary packages to ensure reproducibility, and is more
102 | #   commonly ignored for libraries.
103 | #uv.lock
104 | 
105 | # poetry
106 | #   Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
107 | #   This is especially recommended for binary packages to ensure reproducibility, and is more
108 | #   commonly ignored for libraries.
109 | #   https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
110 | #poetry.lock
111 | 
112 | # pdm
113 | #   Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
114 | #pdm.lock
115 | #   pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
116 | #   in version control.
117 | #   https://pdm.fming.dev/latest/usage/project/#working-with-version-control
118 | .pdm.toml
119 | .pdm-python
120 | .pdm-build/
121 | 
122 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
123 | __pypackages__/
124 | 
125 | # Celery stuff
126 | celerybeat-schedule
127 | celerybeat.pid
128 | 
129 | # SageMath parsed files
130 | *.sage.py
131 | 
132 | # Environments
133 | # .env
134 | .venv
135 | env/
136 | venv/
137 | ENV/
138 | env.bak/
139 | venv.bak/
140 | 
141 | # Spyder project settings
142 | .spyderproject
143 | .spyproject
144 | 
145 | # Rope project settings
146 | .ropeproject
147 | 
148 | # mkdocs documentation
149 | /site
150 | 
151 | # mypy
152 | .mypy_cache/
153 | .dmypy.json
154 | dmypy.json
155 | 
156 | # Pyre type checker
157 | .pyre/
158 | 
159 | # pytype static type analyzer
160 | .pytype/
161 | 
162 | # Cython debug symbols
163 | cython_debug/
164 | 
165 | # PyCharm
166 | #  JetBrains specific template is maintained in a separate JetBrains.gitignore that can
167 | #  be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
168 | #  and can be added to the global gitignore or merged into this file.  For a more nuclear
169 | #  option (not recommended) you can uncomment the following to ignore the entire idea folder.
170 | #.idea/
171 | 
172 | # Ruff stuff:
173 | .ruff_cache/
174 | 
175 | # PyPI configuration file
176 | .pypirc
177 | 
```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
  1 | # ComfyUI MCP Server
  2 | 
  3 | ## 1. Overview
  4 | 
  5 | - A server implementation for integrating ComfyUI with MCP.
  6 | - ⚠️ IMPORTANT: This server requires a running ComfyUI server.
  7 |     - You must either host your own ComfyUI server,
  8 |     - or have access to an existing ComfyUI server address.
  9 | 
 10 | <a href="https://glama.ai/mcp/servers/@Overseer66/comfyui-mcp-server">
 11 |   <img width="380" height="200" src="https://glama.ai/mcp/servers/@Overseer66/comfyui-mcp-server/badge" alt="ComfyUI Server MCP server" />
 12 | </a>
 13 | 
 14 | ---
 15 | 
 16 | ## 2. Debugging
 17 | 
 18 |   ### 2.1 ComfyUI Debugging
 19 | 
 20 |   ```bash
 21 |   python src/test_comfyui.py
 22 |   ```
 23 | 
 24 |   ### 2.2 MCP Debugging
 25 | 
 26 |   ```bash
 27 |   mcp dev src/server.py
 28 |   ```
 29 | 
 30 | ---
 31 | 
 32 | ## 3. Installation and Configuration
 33 | 
 34 |   ### 3.1 ComfyUI Configuration
 35 | 
 36 |   - Edit `src/.env` to set ComfyUI host and port:
 37 | 
 38 |       ```env
 39 |       COMFYUI_HOST=localhost
 40 |       COMFYUI_PORT=8188
 41 |       ```
 42 | 
 43 |   ### 3.2 Adding Custom Workflows
 44 | 
 45 |   - To add new tools, place your workflow JSON files in the `workflows` directory and declare them as new tools in the system.
 46 | 
 47 | ---
 48 | 
 49 | ## 4. Built-in Tools
 50 | 
 51 |   - **text_to_image**
 52 | 
 53 |     - Returns only the URL of the generated image.
 54 |     - To get the actual image:
 55 |         - Use the `download_image` tool, or
 56 |         - Access the URL directly in your browser.
 57 | 
 58 |   - **download_image**
 59 | 
 60 |     - Downloads images generated by other tools (like `text_to_image`) using the image URL.
 61 | 
 62 |   - **run_workflow_with_file**
 63 | 
 64 |     - Run a workflow by providing the path to a workflow JSON file.
 65 | 
 66 |         ```
 67 |         # You should ask to agent like this.
 68 |         Run comfyui workflow with text_to_image.json
 69 |         ```
 70 | 
 71 |     - example image of CursorAI
 72 |       ![](resources/run_workflow_from_file_demo.png)
 73 | 
 74 |   - **run_workflow_with_json**
 75 | 
 76 |     - Run a workflow by providing the workflow JSON data directly.
 77 | 
 78 |         ```
 79 |         # You should ask to agent like this.
 80 |         Run comfyui workflow with this 
 81 |         {
 82 |           "3": {
 83 |               "inputs": {
 84 |                   "seed": 156680208700286,
 85 |                   "steps": 20,
 86 |             ... (workflow JSON example)
 87 |         }
 88 |         ```
 89 | 
 90 | ---
 91 | 
 92 | ## 5. How to Run
 93 | 
 94 |   ### 5.1 Using UV (Recommended)
 95 | 
 96 |   - Example `mcp.json`:
 97 | 
 98 |       ```json
 99 |       {
100 |         "mcpServers": {
101 |           "comfyui": {
102 |             "command": "uv",
103 |             "args": [
104 |               "--directory",
105 |               "PATH/MCP/comfyui",
106 |               "run",
107 |               "--with",
108 |               "mcp",
109 |               "--with",
110 |               "websocket-client",
111 |               "--with",
112 |               "python-dotenv",
113 |               "mcp",
114 |               "run",
115 |               "src/server.py:mcp"
116 |             ]
117 |           }
118 |         }
119 |       }
120 |       ```
121 | 
122 |   ### 5.2 Using Docker
123 | 
124 |   - Downloading images to a local folder with `download_image` may be difficult since the Docker container does not share the host filesystem.
125 |   - When using Docker, consider:
126 |       1. Set `RETURN_URL=false` in `.env` to receive image data as bytes.
127 |       2. Set `COMFYUI_HOST` in `.env` to the appropriate address (e.g., `host.docker.internal` or your server's IP).
128 |       3. Note: Large image payloads may exceed response limits when using binary data.
129 | 
130 |   #### 5.2.1 Build Docker Image
131 | 
132 |   ```bash
133 |   # First build image
134 |   docker image build -t mcp/comfyui .
135 |   ```
136 | 
137 |   ```json
138 |   {
139 |     "mcpServers": {
140 |       "comfyui": {
141 |         "command": "docker",
142 |         "args": [
143 |           "run",
144 |           "-i",
145 |           "--rm",
146 |           "-p",
147 |           "3001:3000",
148 |           "mcp/comfyui"
149 |         ]
150 |       }
151 |     }
152 |   }
153 |   ```
154 | 
155 |   #### 5.2.2 Using Existing Images
156 | 
157 |   Also you can use prebuilt image.
158 | 
159 |   ```json
160 |   {
161 |     "mcpServers": {
162 |       "comfyui": {
163 |         "command": "docker",
164 |         "args": [
165 |           "run",
166 |           "-i",
167 |           "--rm",
168 |           "-p",
169 |           "3001:3000",
170 |           "overseer66/mcp-comfyui"
171 |         ]
172 |       }
173 |     }
174 |   }
175 |   ```
176 | 
177 |   #### 5.2.3 Using SSE Transport
178 | 
179 |   1. Run the SSE server with Docker:
180 | 
181 |       ```bash
182 |       docker run -i --rm -p 8001:8000 overseer66/mcp-comfyui-sse
183 |       ```
184 | 
185 |   2. Configure `mcp.json` (change localhost to your IP or domain if needed):
186 | 
187 |       ```json
188 |       {
189 |         "mcpServers": {
190 |           "comfyui": {
191 |             "url": "http://localhost:8001/sse" 
192 |           }
193 |         }
194 |       }
195 |       ```
196 | 
197 |   > NOTE: When adding new workflows as tools, you need to rebuild and redeploy the Docker images to make them available.
198 | 
199 | ---
```

--------------------------------------------------------------------------------
/src/client/__init__.py:
--------------------------------------------------------------------------------

```python
1 | 
```

--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------

```
1 | mcp[cli]
2 | websocket-client
3 | python-dotenv
4 | 
5 | 
```

--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------

```toml
 1 | [project]
 2 | name = "comfyui-mcp-server"
 3 | version = "0.1.0"
 4 | description = "Add your description here"
 5 | readme = "README.md"
 6 | requires-python = ">=3.12"
 7 | dependencies = [
 8 |     "mcp[cli]>=1.6.0",
 9 |     "python-dotenv>=1.1.0",
10 |     "websocket-client>=1.8.0",
11 | ]
12 | 
```

--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------

```dockerfile
 1 | FROM python:3.12-slim
 2 | 
 3 | WORKDIR /app
 4 | 
 5 | RUN apt-get update && apt-get install -y curl
 6 | 
 7 | COPY . .
 8 | 
 9 | RUN curl -LsSf https://astral.sh/uv/install.sh | sh
10 | 
11 | ENV PATH="/root/.local/bin:${PATH}"
12 | 
13 | RUN uv venv .venv && uv pip install -r pyproject.toml
14 | 
15 | EXPOSE 3000
16 | 
17 | ENTRYPOINT ["uv", "run", "--with", "mcp", "mcp", "run", "src/server.py:mcp"]
18 | 
```

--------------------------------------------------------------------------------
/workflows/text_to_image.json:
--------------------------------------------------------------------------------

```json
  1 | {
  2 |     "3": {
  3 |         "inputs": {
  4 |             "seed": 156680208700286,
  5 |             "steps": 20,
  6 |             "cfg": 8,
  7 |             "sampler_name": "euler",
  8 |             "scheduler": "normal",
  9 |             "denoise": 1,
 10 |             "model": [
 11 |                 "4",
 12 |                 0
 13 |             ],
 14 |             "positive": [
 15 |                 "6",
 16 |                 0
 17 |             ],
 18 |             "negative": [
 19 |                 "7",
 20 |                 0
 21 |             ],
 22 |             "latent_image": [
 23 |                 "5",
 24 |                 0
 25 |             ]
 26 |         },
 27 |         "class_type": "KSampler",
 28 |         "_meta": {
 29 |             "title": "KSampler"
 30 |         }
 31 |     },
 32 |     "4": {
 33 |         "inputs": {
 34 |             "ckpt_name": "v1-5-pruned-emaonly-fp16.safetensors"
 35 |         },
 36 |         "class_type": "CheckpointLoaderSimple",
 37 |         "_meta": {
 38 |             "title": "Load Checkpoint"
 39 |         }
 40 |     },
 41 |     "5": {
 42 |         "inputs": {
 43 |             "width": 512,
 44 |             "height": 512,
 45 |             "batch_size": 1
 46 |         },
 47 |         "class_type": "EmptyLatentImage",
 48 |         "_meta": {
 49 |             "title": "Empty Latent Image"
 50 |         }
 51 |     },
 52 |     "6": {
 53 |         "inputs": {
 54 |             "text": "beautiful scenery nature glass bottle landscape, , purple galaxy bottle,",
 55 |             "clip": [
 56 |                 "4",
 57 |                 1
 58 |             ]
 59 |         },
 60 |         "class_type": "CLIPTextEncode",
 61 |         "_meta": {
 62 |             "title": "CLIP Text Encode (Prompt)"
 63 |         }
 64 |     },
 65 |     "7": {
 66 |         "inputs": {
 67 |             "text": "text, watermark",
 68 |             "clip": [
 69 |                 "4",
 70 |                 1
 71 |             ]
 72 |         },
 73 |         "class_type": "CLIPTextEncode",
 74 |         "_meta": {
 75 |             "title": "CLIP Text Encode (Prompt)"
 76 |         }
 77 |     },
 78 |     "8": {
 79 |         "inputs": {
 80 |             "samples": [
 81 |                 "3",
 82 |                 0
 83 |             ],
 84 |             "vae": [
 85 |                 "4",
 86 |                 2
 87 |             ]
 88 |         },
 89 |         "class_type": "VAEDecode",
 90 |         "_meta": {
 91 |             "title": "VAE Decode"
 92 |         }
 93 |     },
 94 |     "9": {
 95 |         "inputs": {
 96 |             "filename_prefix": "ComfyUI",
 97 |             "images": [
 98 |                 "8",
 99 |                 0
100 |             ]
101 |         },
102 |         "class_type": "SaveImage",
103 |         "_meta": {
104 |             "title": "Save Image"
105 |         }
106 |     }
107 | }
```

--------------------------------------------------------------------------------
/src/server.py:
--------------------------------------------------------------------------------

```python
 1 | import os
 2 | import json
 3 | import urllib.request
 4 | import urllib.parse
 5 | from typing import Any
 6 | from client.comfyui import ComfyUI
 7 | from mcp.server.fastmcp import FastMCP
 8 | from dotenv import load_dotenv
 9 | 
10 | load_dotenv()
11 | 
12 | mcp = FastMCP("comfyui")
13 | 
14 | @mcp.tool()
15 | async def text_to_image(prompt: str, seed: int, steps: int, cfg: float, denoise: float) -> Any:
16 |     """Generate an image from a prompt.
17 |     
18 |     Args:
19 |         prompt: The prompt to generate the image from.
20 |         seed: The seed to use for the image generation.
21 |         steps: The number of steps to use for the image generation.
22 |         cfg: The CFG scale to use for the image generation.
23 |         denoise: The denoise strength to use for the image generation.
24 |     """
25 |     auth = os.environ.get("COMFYUI_AUTHENTICATION")
26 |     comfy = ComfyUI(
27 |         url=f'http://{os.environ.get("COMFYUI_HOST", "localhost")}:{os.environ.get("COMFYUI_PORT", 8188)}',
28 |         authentication=auth
29 |     )
30 |     images = await comfy.process_workflow("text_to_image", {"prompt": prompt, "seed": seed, "steps": steps, "cfg": cfg, "denoise": denoise}, return_url=os.environ.get("RETURN_URL", "true").lower() == "true")
31 |     return images
32 | 
33 | @mcp.tool()
34 | async def download_image(url: str, save_path: str) -> Any:
35 |     """Download an image from a URL and save it to a file.
36 |     
37 |     Args:
38 |         url: The URL of the image to download.
39 |         save_path: The absolute path to save the image to. Must be an absolute path, otherwise the image will be saved relative to the server location.
40 |     """
41 |     urllib.request.urlretrieve(url, save_path)
42 |     return {"success": True}
43 | 
44 | @mcp.tool()
45 | async def run_workflow_from_file(file_path: str) -> Any:
46 |     """Run a workflow from a file.
47 |     
48 |     Args:
49 |         file_path: The absolute path to the file to run.
50 |     """
51 |     with open(file_path, "r", encoding="utf-8") as f:
52 |         workflow = json.load(f)
53 |     
54 |     auth = os.environ.get("COMFYUI_AUTHENTICATION")
55 |     comfy = ComfyUI(
56 |         url=f'http://{os.environ.get("COMFYUI_HOST", "localhost")}:{os.environ.get("COMFYUI_PORT", 8188)}',
57 |         authentication=auth
58 |     )
59 |     images = await comfy.process_workflow(workflow, {}, return_url=os.environ.get("RETURN_URL", "true").lower() == "true")
60 |     return images
61 | 
62 | @mcp.tool()
63 | async def run_workflow_from_json(json_data: dict) -> Any:
64 |     """Run a workflow from a JSON data.
65 |     
66 |     Args:
67 |         json_data: The JSON data to run.
68 |     """
69 |     workflow = json_data
70 |     
71 |     auth = os.environ.get("COMFYUI_AUTHENTICATION")
72 |     comfy = ComfyUI(
73 |         url=f'http://{os.environ.get("COMFYUI_HOST", "localhost")}:{os.environ.get("COMFYUI_PORT", 8188)}',
74 |         authentication=auth
75 |     )
76 |     images = await comfy.process_workflow(workflow, {}, return_url=os.environ.get("RETURN_URL", "true").lower() == "true")
77 |     return images
78 | 
79 | if __name__ == "__main__":
80 |     mcp.run(transport=os.environ.get("MCP_TRANSPORT", "stdio"))
81 | 
```

--------------------------------------------------------------------------------
/src/client/comfyui.py:
--------------------------------------------------------------------------------

```python
  1 | import os
  2 | import websocket
  3 | import json
  4 | import uuid
  5 | import urllib.parse
  6 | import urllib.request
  7 | from typing import Dict, Any
  8 | 
  9 | class ComfyUI:
 10 |     def __init__(self, url: str, authentication: str = None):
 11 |         self.url = url
 12 |         self.authentication = authentication
 13 |         self.client_id = str(uuid.uuid4())
 14 |         self.headers = {
 15 |             "Content-Type": "application/json"
 16 |         }
 17 |         if authentication:
 18 |             self.headers["Authorization"] = authentication
 19 | 
 20 |     def get_image(self, filename, subfolder, folder_type):
 21 |         data = {"filename": filename, "subfolder": subfolder, "type": folder_type}
 22 |         url_values = urllib.parse.urlencode(data)
 23 |         url = f"{self.url}/view?{url_values}"
 24 |         req = urllib.request.Request(url, headers=self.headers)
 25 |         with urllib.request.urlopen(req) as response:
 26 |             return response.read()
 27 | 
 28 |     def get_history(self, prompt_id):
 29 |         url = f"{self.url}/history/{prompt_id}"
 30 |         req = urllib.request.Request(url, headers=self.headers)
 31 |         with urllib.request.urlopen(req) as response:
 32 |             return json.loads(response.read())
 33 |     
 34 |     def queue_prompt(self, prompt):
 35 |         p = {"prompt": prompt, "client_id": self.client_id}
 36 |         data = json.dumps(p).encode("utf-8")
 37 |         req = urllib.request.Request(
 38 |             f"{self.url}/prompt",
 39 |             headers=self.headers,
 40 |             data=data
 41 |         )
 42 |         return json.loads(urllib.request.urlopen(req).read())
 43 |     
 44 |     async def process_workflow(self, workflow: Any, params: Dict[str, Any], return_url: bool = False):
 45 |         if isinstance(workflow, str):
 46 |             workflow_path = os.path.join(os.environ.get("WORKFLOW_DIR", "workflows"), f"{workflow}.json")
 47 |             if not os.path.exists(workflow_path):
 48 |                 raise Exception(f"Workflow {workflow} not found")
 49 |             with open(workflow_path, "r", encoding='utf-8') as f:
 50 |                 prompt = json.load(f)
 51 |         else:
 52 |             prompt = workflow
 53 | 
 54 |         self.update_workflow_params(prompt, params)
 55 | 
 56 |         ws = websocket.WebSocket()
 57 |         ws_url = f"ws://{os.environ.get("COMFYUI_HOST", "localhost")}:{os.environ.get("COMFYUI_PORT", 8188)}/ws?clientId={self.client_id}"
 58 |         
 59 |         if self.authentication:
 60 |             ws.connect(ws_url, header=[f"Authorization: {self.authentication}"])
 61 |         else:
 62 |             ws.connect(ws_url)
 63 | 
 64 |         try:
 65 |             images = self.get_images(ws, prompt, return_url)
 66 |             return images
 67 |         finally:
 68 |             ws.close()
 69 | 
 70 |     def get_images(self, ws, prompt, return_url):
 71 |         prompt_id = self.queue_prompt(prompt)["prompt_id"]
 72 |         output_images = {}
 73 |         
 74 |         while True:
 75 |             out = ws.recv()
 76 |             if isinstance(out, str):
 77 |                 message = json.loads(out)
 78 |                 if message["type"] == "executing":
 79 |                     data = message["data"]
 80 |                     if data["node"] is None and data["prompt_id"] == prompt_id:
 81 |                         break
 82 |             else:
 83 |                 continue
 84 | 
 85 |         history = self.get_history(prompt_id)[prompt_id]
 86 |         for node_id in history["outputs"]:
 87 |             node_output = history["outputs"][node_id]
 88 |             if "images" in node_output:
 89 |                 if return_url:
 90 |                     output_images[node_id] = []
 91 |                     for image in node_output["images"]:
 92 |                         data = {"filename": image["filename"], "subfolder": image["subfolder"], "type": image["type"]}
 93 |                         url_values = urllib.parse.urlencode(data)
 94 |                         url = f"{self.url}/view?{url_values}"
 95 |                         output_images[node_id].append(url)
 96 |                 else:
 97 |                     output_images[node_id] = [
 98 |                         self.get_image(image["filename"], image["subfolder"], image["type"])
 99 |                         for image in node_output["images"]
100 |                     ]
101 | 
102 |         return output_images
103 | 
104 |     def update_workflow_params(self, prompt, params):
105 |         if not params:
106 |             return
107 | 
108 |         for node in prompt.values():
109 |             if node["class_type"] == "CLIPTextEncode" and "text" in params:
110 |                 if isinstance(node["inputs"]["text"], str):
111 |                     node["inputs"]["text"] = params["text"]
112 |             elif node["class_type"] == "KSampler":
113 |                 if "seed" in params:
114 |                     node["inputs"]["seed"] = params["seed"]
115 |                 if "steps" in params:
116 |                     node["inputs"]["steps"] = params["steps"]
117 |                 if "cfg" in params:
118 |                     node["inputs"]["cfg"] = params["cfg"]
119 |                 if "denoise" in params:
120 |                     node["inputs"]["denoise"] = params["denoise"]
121 |             
122 |             elif node["class_type"] == "LoadImage" and "image" in params:
123 |                 node["inputs"]["image"] = params["image"]
124 | 
```