Initial commit: ComfyUI MCP Server with Node Integration

This is a complete rewrite of the MCP server configuration system:

Features:
- ComfyUI custom node for visual MCP configuration
- Bilingual documentation (English/Chinese)
- Auto-installation and auto-configuration for AI assistants
- Support for Claude Code, Claude Desktop, and Gemini CLI
- Simplified setup without manual bat files

Architecture:
- src/comfy_mcp_server/ - MCP server core (auto-started by AI assistants)
- nodes/ - ComfyUI custom node for configuration
- workflow/ - Example workflow files
- Merged pyproject.toml for both MCP server and ComfyUI node

Based on the original work by lalanikarim/comfy-mcp-server
Refactored and enhanced by dseditor for easier ComfyUI integration
This commit is contained in:
dseditor
2025-12-09 21:01:12 +08:00
commit af3c98025c
12 changed files with 2765 additions and 0 deletions
+71
View File
@@ -0,0 +1,71 @@
# Python generated files
__pycache__/
*.py[cod]
*$py.class
*.so
*.egg
*.egg-info/
dist/
build/
wheels/
pip-wheel-metadata/
# Virtual environments
.venv/
venv/
env/
ENV/
# IDE and Editor files
.vscode/
.idea/
.aider*
*.swp
*.swo
*~
.envrc
# User-specific Claude Code configuration
.claude/
# Generated configuration files (created by the node at runtime)
mcp_config.ini
claude_desktop_mcp_config*.json
gemini_settings*.json
temp_mcp_config.json
# Generated images
generated_images/
# Log files
*.log
mcp_server.log
# Backup files
backup/
*.backup
*.bak
*.old
# OS specific files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
Desktop.ini
# Development lock files (optional - uncomment if you want to track it)
# uv.lock
# Test and temporary files
.pytest_cache/
.coverage
htmlcov/
*.cover
.hypothesis/
.tox/
*.zip
GIT_COMMIT_GUIDE.md
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Karim Lalani
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+221
View File
@@ -0,0 +1,221 @@
# ComfyUI MCP Server
[中文说明](#中文说明) | [English](#english)
---
## English
MCP Server for ComfyUI - Seamlessly integrate AI assistants (Claude Code, Claude Desktop, Gemini CLI) with ComfyUI through the Model Context Protocol.
### Features
- 🎨 **ComfyUI Integration**: Connect AI assistants to ComfyUI via MCP protocol
- 🔧 **Custom Node**: Configure MCP parameters visually within ComfyUI
- 📦 **Portable Setup**: Use relative paths for easy environment migration
- 🚀 **Auto-Start**: MCP Server launches automatically when AI assistants need it
- ⚙️ **Auto-Configuration**: Automatically install and configure MCP for AI assistants
### Quick Start
#### 1. Start ComfyUI
Simply start ComfyUI with your normal startup script (no special MCP launcher needed):
\`\`\`batch
# Windows
run_nvidia_gpu.bat
# Or any other ComfyUI startup script you normally use
\`\`\`
#### 2. Configure MCP in ComfyUI
Add the **MCP Config Generator** node to your workflow:
1. Add \`MCP Config Generator\` node in ComfyUI
2. Configure the node parameters:
- **workflow_file**: Select your workflow JSON file
- **prompt_node_id**: Node ID for text input (e.g., "45")
- **output_node_id**: Node ID for image output (e.g., "9")
- **comfy_url**: ComfyUI server URL (default: \`http://127.0.0.1:8188\`)
- **auto_install**: ✅ Enable (for first-time setup)
- **auto_update_claude_code**: ✅ Enable (if using Claude Code)
- **auto_update_claude_desktop**: ✅ Enable (if using Claude Desktop)
- **auto_update_gemini_cli**: ✅ Enable (if using Gemini CLI)
3. Execute the workflow
The node will automatically:
- Install the MCP server module
- Generate configuration files
- Update AI assistant configuration files
#### 3. Use with AI Assistants
That's it! The MCP server will automatically start when you use the AI assistant.
**In Claude Code or Claude Desktop:**
\`\`\`
Generate an image of a cat in a sunny garden
\`\`\`
The AI assistant will automatically:
1. Start the MCP server (if not running)
2. Call ComfyUI to generate the image
3. Return the generated image
### Manual Installation (Optional)
If auto-installation doesn't work, you can install manually:
\`\`\`batch
cd path/to/ComfyUI/custom_nodes/ComfyUI-MCP
python -m pip install -r requirements.txt
python -m pip install -e .
\`\`\`
### System Requirements
- Python >= 3.10
- ComfyUI
- Claude Code, Claude Desktop, or Gemini CLI
### Architecture
This project uses a dual-layer architecture:
\`\`\`
ComfyUI-MCP/
├── src/comfy_mcp_server/ ← MCP Server core (auto-started by AI assistants)
├── nodes/mcp_config_node.py ← ComfyUI node (for configuration)
├── workflow/ ← Workflow JSON files
├── pyproject.toml ← Package configuration
└── requirements.txt ← Dependencies
\`\`\`
### Troubleshooting
**MCP server not working?**
1. Check that \`auto_install\` is enabled in the node
2. Restart ComfyUI after first installation
3. Verify the module is installed: \`python -m pip list | grep comfy-mcp-server\`
**AI assistant can't find MCP server?**
1. Check that \`auto_update_*\` is enabled for your AI assistant
2. Restart the AI assistant after configuration
3. Manually check the configuration file:
- Claude Code: \`~/.config/claude-code/mcp.json\`
- Claude Desktop: \`%APPDATA%/Claude/claude_desktop_config.json\`
### License
MIT
---
## 中文说明
ComfyUI MCP 服务器 - 通过模型上下文协议(MCP)无缝整合 AI 助手(Claude Code、Claude Desktop、Gemini CLI)与 ComfyUI。
### 功能特点
- 🎨 **ComfyUI 整合**: 通过 MCP 协议连接 AI 助手与 ComfyUI
- 🔧 **自定义节点**: 在 ComfyUI 中可视化配置 MCP 参数
- 📦 **便携式设置**: 使用相对路径,便于环境迁移
- 🚀 **自动启动**: AI 助手需要时自动启动 MCP 服务器
- ⚙️ **自动配置**: 自动安装并配置 MCP 到 AI 助手
### 快速开始
#### 1. 启动 ComfyUI
使用普通的启动脚本即可(不需要特殊的 MCP 启动器):
\`\`\`batch
# Windows
run_nvidia_gpu.bat
# 或者使用你平时使用的任何 ComfyUI 启动脚本
\`\`\`
#### 2. 在 ComfyUI 中配置 MCP
在工作流中添加 **MCP Config Generator** 节点:
1. 在 ComfyUI 中添加 \`MCP Config Generator\` 节点
2. 配置节点参数:
- **workflow_file**: 选择工作流 JSON 文件
- **prompt_node_id**: 文本输入节点 ID(例如 "45")
- **output_node_id**: 图像输出节点 ID(例如 "9")
- **comfy_url**: ComfyUI 服务器 URL(默认:\`http://127.0.0.1:8188\`)
- **auto_install**: ✅ 启用(首次设置时)
- **auto_update_claude_code**: ✅ 启用(如果使用 Claude Code)
- **auto_update_claude_desktop**: ✅ 启用(如果使用 Claude Desktop)
- **auto_update_gemini_cli**: ✅ 启用(如果使用 Gemini CLI)
3. 执行工作流
节点会自动:
- 安装 MCP 服务器模块
- 生成配置文件
- 更新 AI 助手配置文件
#### 3. 在 AI 助手中使用
完成!MCP 服务器会在你使用 AI 助手时自动启动。
**在 Claude Code 或 Claude Desktop 中:**
\`\`\`
生成一张阳光花园中的猫的图片
\`\`\`
AI 助手会自动:
1. 启动 MCP 服务器(如果未运行)
2. 调用 ComfyUI 生成图片
3. 返回生成的图片
### 手动安装(可选)
如果自动安装不起作用,可以手动安装:
\`\`\`batch
cd ComfyUI所在路径/custom_nodes/ComfyUI-MCP
python -m pip install -r requirements.txt
python -m pip install -e .
\`\`\`
### 系统要求
- Python >= 3.10
- ComfyUI
- Claude Code、Claude Desktop 或 Gemini CLI
### 架构说明
本项目采用双层架构:
\`\`\`
ComfyUI-MCP/
├── src/comfy_mcp_server/ ← MCP 服务器核心(由 AI 助手自动启动)
├── nodes/mcp_config_node.py ← ComfyUI 节点(用于配置)
├── workflow/ ← 工作流 JSON 文件
├── pyproject.toml ← 包配置
└── requirements.txt ← 依赖项
\`\`\`
### 故障排除
**MCP 服务器无法工作?**
1. 检查节点中是否启用了 \`auto_install\`
2. 首次安装后重启 ComfyUI
3. 验证模块已安装:\`python -m pip list | grep comfy-mcp-server\`
**AI 助手找不到 MCP 服务器?**
1. 检查是否为你的 AI 助手启用了 \`auto_update_*\`
2. 配置后重启 AI 助手
3. 手动检查配置文件:
- Claude Code: \`~/.config/claude-code/mcp.json\`
- Claude Desktop: \`%APPDATA%/Claude/claude_desktop_config.json\`
### 许可证
MIT
+12
View File
@@ -0,0 +1,12 @@
"""
ComfyUI-MCP Custom Nodes
為 ComfyUI 提供 MCP (Model Context Protocol) 整合節點
"""
from .nodes.mcp_config_node import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS
__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS']
# 版本資訊
WEB_DIRECTORY = "./web"
__version__ = "1.0.0"
+1
View File
@@ -0,0 +1 @@
# ComfyUI-MCP Nodes Package
+529
View File
@@ -0,0 +1,529 @@
import os
import json
import subprocess
import sys
class MCPConfigGenerator:
"""
ComfyUI Node: Generate MCP Server Configuration
Select workflow files from workflow folder and configure MCP parameters
Includes auto-install, Claude Code config, multi-format output, etc.
"""
RETURN_TYPES = ("STRING", "STRING")
RETURN_NAMES = ("status_text", "json_config")
FUNCTION = "generate_config"
CATEGORY = "MCP"
OUTPUT_NODE = True
@classmethod
def INPUT_TYPES(cls):
# Get all JSON files from workflow folder
workflow_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), "workflow")
workflow_files = ["None"]
if os.path.exists(workflow_dir):
workflow_files = ["None"] + [
f for f in os.listdir(workflow_dir)
if f.endswith('.json')
]
return {
"required": {
"workflow_file": (workflow_files,),
"prompt_node_id": ("STRING", {
"default": "45",
"multiline": False
}),
"output_node_id": ("STRING", {
"default": "9",
"multiline": False
}),
"comfy_url": ("STRING", {
"default": "http://127.0.0.1:8188",
"multiline": False
}),
"max_poll_attempts": ("INT", {
"default": 60,
"min": 10,
"max": 300,
"step": 1
}),
"poll_interval": ("INT", {
"default": 2,
"min": 1,
"max": 10,
"step": 1
}),
"output_mode": (["url", "file"],),
"config_format": (["ClaudeDesktop", "Gemini"],),
"auto_install": ("BOOLEAN", {"default": False}),
"auto_update_claude_code": ("BOOLEAN", {"default": False}),
"auto_update_claude_desktop": ("BOOLEAN", {"default": False}),
"auto_update_gemini_cli": ("BOOLEAN", {"default": False}),
}
}
def generate_config(self, workflow_file, prompt_node_id, output_node_id,
comfy_url, max_poll_attempts, poll_interval, output_mode,
config_format, auto_install, auto_update_claude_code,
auto_update_claude_desktop, auto_update_gemini_cli):
"""
Generate MCP config, update config files, auto-install module, auto-update AI configs
"""
status_messages = []
warnings = []
# Get project root directory
node_dir = os.path.dirname(os.path.dirname(__file__))
# Determine full path of workflow file
if workflow_file == "None":
# Default workflow should also be in workflow folder
workflow_path = os.path.join(node_dir, "workflow", "image_z_image_turbo.json")
workflow_file = "image_z_image_turbo.json"
else:
workflow_path = os.path.join(node_dir, "workflow", workflow_file)
# Verify workflow file exists
if not os.path.exists(workflow_path):
error_msg = f"Error: Workflow file not found: {workflow_path}"
return (error_msg, "{}")
# Get absolute path of python_embeded
python_exe = os.path.abspath(
os.path.join(node_dir, "..", "..", "..", "python_embeded", "python.exe")
)
# Check if Python exists
if not os.path.exists(python_exe):
error_msg = f"Error: Python not found: {python_exe}"
return (error_msg, "{}")
# 1. Auto-install module (if enabled)
if auto_install:
status_messages.append("=" * 50)
status_messages.append("Auto-Install MCP Module")
status_messages.append("=" * 50)
warnings.append("⚠ Restart ComfyUI after installation to take effect!")
install_result = self._install_module(python_exe, node_dir)
status_messages.append(install_result)
status_messages.append("")
# Build MCP base configuration
mcp_env_config = {
"COMFY_URL": comfy_url,
"COMFY_URL_EXTERNAL": comfy_url,
"COMFY_WORKFLOW_JSON_FILE": workflow_path,
"PROMPT_NODE_ID": str(prompt_node_id),
"OUTPUT_NODE_ID": str(output_node_id),
"OUTPUT_MODE": output_mode,
"COMFY_MAX_POLL_ATTEMPTS": str(max_poll_attempts),
"COMFY_POLL_INTERVAL": str(poll_interval)
}
# Generate configuration based on format
if config_format == "ClaudeDesktop":
mcp_config = {
"mcpServers": {
"comfyui-image-generator": {
"command": python_exe,
"args": ["-m", "comfy_mcp_server"],
"cwd": node_dir,
"env": mcp_env_config
}
}
}
else: # Gemini
mcp_config = {
"mcp_servers": {
"comfyui-image-generator": {
"command": python_exe,
"args": ["-m", "comfy_mcp_server"],
"cwd": node_dir,
"env": mcp_env_config
}
}
}
# 2. Auto-update AI platform configs (if enabled)
if auto_update_claude_code:
status_messages.append("=" * 50)
status_messages.append("Auto-Update: Claude Code (User Scope)")
status_messages.append("=" * 50)
claude_code_config = {
"command": python_exe,
"args": ["-m", "comfy_mcp_server"],
"cwd": node_dir,
"env": mcp_env_config
}
result = self._update_claude_code(claude_code_config)
status_messages.append(result)
status_messages.append("")
if auto_update_claude_desktop:
status_messages.append("=" * 50)
status_messages.append("Auto-Update: Claude Desktop")
status_messages.append("=" * 50)
claude_desktop_config = {
"command": python_exe,
"args": ["-m", "comfy_mcp_server"],
"cwd": node_dir,
"env": mcp_env_config
}
result = self._update_claude_desktop(claude_desktop_config)
status_messages.append(result)
status_messages.append("")
if auto_update_gemini_cli:
status_messages.append("=" * 50)
status_messages.append("Auto-Update: Gemini CLI")
status_messages.append("=" * 50)
gemini_config = {
"command": python_exe,
"args": ["-m", "comfy_mcp_server"],
"env": mcp_env_config,
"timeout": 120000,
"trust": False
}
result = self._update_gemini_cli(gemini_config)
status_messages.append(result)
status_messages.append("")
# 3. Save JSON config file
if config_format == "ClaudeDesktop":
json_filename = f"claude_desktop_mcp_config_{workflow_file.replace('.json', '')}.json"
else:
json_filename = f"gemini_settings_{workflow_file.replace('.json', '')}.json"
json_path = os.path.join(node_dir, json_filename)
try:
with open(json_path, 'w', encoding='utf-8') as f:
json.dump(mcp_config, f, indent=2, ensure_ascii=False)
status_messages.append("=" * 50)
status_messages.append("Configuration File Generated")
status_messages.append("=" * 50)
status_messages.append(f"✓ {json_filename} generated")
status_messages.append(f" Location: {json_path}")
status_messages.append("")
# 4. Update mcp_config.ini config file
config_ini_path = os.path.join(node_dir, "mcp_config.ini")
try:
with open(config_ini_path, 'w', encoding='utf-8') as f:
f.write(f"WORKFLOW_FILE={workflow_path}\n")
f.write(f"PROMPT_NODE_ID={prompt_node_id}\n")
f.write(f"OUTPUT_NODE_ID={output_node_id}\n")
f.write(f"COMFY_URL={comfy_url}\n")
f.write(f"OUTPUT_MODE={output_mode}\n")
f.write(f"MAX_POLL_ATTEMPTS={max_poll_attempts}\n")
f.write(f"POLL_INTERVAL={poll_interval}\n")
status_messages.append("✓ mcp_config.ini updated")
except Exception as e:
status_messages.append(f"⚠ Failed to update mcp_config.ini: {e}")
# 5. Check MCP status
status_messages.append("")
status_messages.append("=" * 50)
status_messages.append("MCP Status Check")
status_messages.append("=" * 50)
mcp_status = self._check_mcp_status(python_exe, node_dir)
status_messages.append(mcp_status)
# 6. Generate configuration summary
status_messages.append("")
status_messages.append("=" * 50)
status_messages.append("Configuration Summary")
status_messages.append("=" * 50)
status_messages.append(f"Workflow: {workflow_file}")
status_messages.append(f"Prompt Node ID: {prompt_node_id}")
status_messages.append(f"Output Node ID: {output_node_id}")
status_messages.append(f"ComfyUI URL: {comfy_url}")
status_messages.append(f"Output Mode: {output_mode}")
status_messages.append(f"Config Format: {config_format}")
status_messages.append(f"Auto Install: {'Yes' if auto_install else 'No'}")
status_messages.append(f"Auto Update Claude Code: {'Yes' if auto_update_claude_code else 'No'}")
status_messages.append(f"Auto Update Claude Desktop: {'Yes' if auto_update_claude_desktop else 'No'}")
status_messages.append(f"Auto Update Gemini CLI: {'Yes' if auto_update_gemini_cli else 'No'}")
# 7. Add usage instructions
if config_format == "ClaudeDesktop":
status_messages.append("")
status_messages.append("=" * 50)
status_messages.append("Claude Desktop Configuration Guide")
status_messages.append("=" * 50)
status_messages.append("Merge generated JSON content to:")
status_messages.append("Windows: %APPDATA%\\Claude\\claude_desktop_config.json")
status_messages.append("macOS: ~/Library/Application Support/Claude/claude_desktop_config.json")
else: # Gemini
status_messages.append("")
status_messages.append("=" * 50)
status_messages.append("Gemini Configuration Guide")
status_messages.append("=" * 50)
status_messages.append("Add generated JSON content to Gemini's settings.json")
# 8. Add warnings
if warnings:
status_messages.append("")
status_messages.append("=" * 50)
status_messages.append("Important Notice")
status_messages.append("=" * 50)
for warning in warnings:
status_messages.append(warning)
# Combine final output
final_status = "\n".join(status_messages)
json_output = json.dumps(mcp_config, indent=2, ensure_ascii=False)
return (final_status, json_output)
except Exception as e:
error_msg = f"Error: Cannot generate config file - {str(e)}"
return (error_msg, "{}")
def _install_module(self, python_exe, node_dir):
"""Install MCP module"""
try:
# Install hatchling
result = subprocess.run(
[python_exe, "-m", "pip", "install", "hatchling", "--quiet"],
capture_output=True,
text=True,
cwd=node_dir
)
# Install comfy-mcp-server
result = subprocess.run(
[python_exe, "-m", "pip", "install", "-e", "."],
capture_output=True,
text=True,
cwd=node_dir
)
if result.returncode == 0:
# Verify installation
verify_result = subprocess.run(
[python_exe, "-m", "comfy_mcp_server", "--help"],
capture_output=True,
text=True,
cwd=node_dir
)
if verify_result.returncode == 0:
return "✓ MCP module installed successfully and can run properly"
else:
return "⚠ Module installed but cannot execute, may need to check environment"
else:
return f"✗ Installation failed: {result.stderr}"
except Exception as e:
return f"✗ Error during installation: {str(e)}"
def _update_claude_code(self, config):
"""Auto-update Claude Code MCP configuration (User Scope)"""
try:
json_str = json.dumps(config)
# Escape double quotes in JSON string
json_str_escaped = json_str.replace('"', '\\"')
# Windows requires shell=True to execute .cmd files
cmd = f'claude mcp add-json --scope user comfyui-image-generator "{json_str_escaped}"'
result = subprocess.run(
cmd,
capture_output=True,
text=True,
shell=True,
timeout=30
)
if result.returncode == 0:
return "✓ Successfully added/updated MCP configuration to Claude Code (User Scope)"
else:
return f"✗ Failed to update Claude Code: {result.stderr}\n Hint: Please ensure Claude Code CLI is installed"
except FileNotFoundError:
return "✗ Claude Code CLI not found. Please install Claude Code first"
except subprocess.TimeoutExpired:
return "✗ Claude Code update timeout (network may be slow)"
except Exception as e:
return f"✗ Error updating Claude Code: {str(e)}"
def _update_claude_desktop(self, config):
"""Auto-update Claude Desktop config file"""
try:
# Get username
username = os.getenv('USERNAME') or os.getenv('USER')
if not username:
return "✗ Cannot determine username for Claude Desktop config path"
# Claude Desktop config path
config_path = os.path.join(
"C:", "Users", username, "AppData", "Roaming", "Claude", "claude_desktop_config.json"
)
# Check if config file exists
if not os.path.exists(config_path):
# Create directory and initial config
os.makedirs(os.path.dirname(config_path), exist_ok=True)
existing_config = {"mcpServers": {}}
else:
# Read existing config
with open(config_path, 'r', encoding='utf-8') as f:
existing_config = json.load(f)
# Ensure mcpServers key exists
if "mcpServers" not in existing_config:
existing_config["mcpServers"] = {}
# Update or add comfyui-image-generator config
existing_config["mcpServers"]["comfyui-image-generator"] = config
# Write back to file
with open(config_path, 'w', encoding='utf-8') as f:
json.dump(existing_config, f, indent=2, ensure_ascii=False)
return f"✓ Successfully updated Claude Desktop config\n Path: {config_path}"
except PermissionError:
return f"✗ Permission denied: Cannot write to {config_path}"
except Exception as e:
return f"✗ Error updating Claude Desktop config: {str(e)}"
def _update_gemini_cli(self, config):
"""Auto-update Gemini CLI config file"""
try:
# Get username
username = os.getenv('USERNAME') or os.getenv('USER')
if not username:
return "✗ Cannot determine username for Gemini CLI config path"
# Gemini CLI config path
config_path = os.path.join(
"C:", "Users", username, ".gemini", "settings.json"
)
# Check if config file exists
if not os.path.exists(config_path):
# Create directory and initial config
os.makedirs(os.path.dirname(config_path), exist_ok=True)
existing_config = {"mcpServers": {}}
else:
# Read existing config
with open(config_path, 'r', encoding='utf-8') as f:
existing_config = json.load(f)
# Ensure mcpServers key exists
if "mcpServers" not in existing_config:
existing_config["mcpServers"] = {}
# Update or add comfyui-image-generator config
existing_config["mcpServers"]["comfyui-image-generator"] = config
# Write back to file
with open(config_path, 'w', encoding='utf-8') as f:
json.dump(existing_config, f, indent=2, ensure_ascii=False)
return f"✓ Successfully updated Gemini CLI config\n Path: {config_path}"
except PermissionError:
return f"✗ Permission denied: Cannot write to {config_path}"
except Exception as e:
return f"✗ Error updating Gemini CLI config: {str(e)}"
def _check_mcp_status(self, python_exe, node_dir):
"""Check MCP module status"""
status_lines = []
# 1. Check if module is installed
try:
result = subprocess.run(
[python_exe, "-m", "pip", "show", "comfy-mcp-server"],
capture_output=True,
text=True,
cwd=node_dir
)
if result.returncode == 0:
status_lines.append("✓ MCP module is installed")
# Get version info
for line in result.stdout.split('\n'):
if line.startswith('Version:'):
status_lines.append(f" Version: {line.split(':', 1)[1].strip()}")
break
else:
status_lines.append("✗ MCP module is not installed")
except Exception as e:
status_lines.append(f"✗ Error checking module installation: {str(e)}")
# 2. Check if module can be executed
try:
# Test if module can be loaded
src_path = os.path.join(node_dir, "src")
verify_result = subprocess.run(
[python_exe, "-c",
f"import sys; sys.path.insert(0, r'{src_path}'); from comfy_mcp_server import mcp; print('OK')"],
capture_output=True,
text=True,
cwd=node_dir
)
if verify_result.returncode == 0 and "OK" in verify_result.stdout:
status_lines.append("✓ MCP module can be loaded successfully")
else:
status_lines.append("✗ MCP module cannot be loaded")
if verify_result.stderr:
status_lines.append(f" Error: {verify_result.stderr.strip()[:100]}")
except Exception as e:
status_lines.append(f"✗ Error testing module: {str(e)}")
# 3. Check Claude Code MCP config (handle safely)
try:
# Windows requires shell=True to execute .cmd files
claude_result = subprocess.run(
"claude mcp list",
capture_output=True,
text=True,
timeout=30,
shell=True
)
if claude_result.returncode == 0:
if "comfyui-image-generator" in claude_result.stdout:
status_lines.append("✓ Claude Code MCP is configured")
else:
status_lines.append("ℹ Claude Code MCP is not configured")
else:
status_lines.append("ℹ Claude Code CLI error")
except FileNotFoundError:
status_lines.append("ℹ Claude Code CLI not found (optional)")
except subprocess.TimeoutExpired:
status_lines.append("ℹ Claude Code CLI timeout (network may be slow, but MCP might still work)")
except Exception as e:
status_lines.append(f"ℹ Cannot check Claude Code status (but MCP might still work)")
return "\n".join(status_lines)
# Node mappings
NODE_CLASS_MAPPINGS = {
"MCPConfigGenerator": MCPConfigGenerator
}
# Node display names
NODE_DISPLAY_NAME_MAPPINGS = {
"MCPConfigGenerator": "MCP Config Generator"
}
+51
View File
@@ -0,0 +1,51 @@
[project]
name = "comfy-mcp-server"
version = "0.0.1"
description = "Simple MCP Server for ComfyUI text to image Workflow - ComfyUI Node Integration , Based on lalanikarim/comfy-mcp-server work"
readme = "README.md"
requires-python = ">=3.10"
license = "MIT"
license-files = ["LICENSE"]
dependencies = [
"langchain>=0.3.19",
"langchain-ollama>=0.2.3",
"mcp[cli]>=1.2.1",
"Pillow>=10.0.0",
]
authors = [
{name = "dseditor", email = "comfyuimixer1231@gmail.com"},
]
maintainers = [
{name = "dseditor", email = "comfyuimixer1231@gmail.com"},
]
[project.urls]
Homepage = "https://github.com/dseditor/Comfy-MCP/"
Repository = "https://github.com/dseditor/Comfy-MCP/"
Documentation = "https://github.com/dseditor/Comfy-MCP/wiki"
"Bug Tracker" = "https://github.com/dseditor/Comfy-MCP/issues"
"Original Project" = "https://github.com/lalanikarim/comfy-mcp-server"
# MCP Server entry point - required for `python -m comfy_mcp_server`
[project.scripts]
comfy-mcp-server = "comfy_mcp_server:run_server"
# Build system configuration - required for pip install
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
# Package source location - tells pip where to find the code
[tool.hatch.build.targets.wheel]
packages = ["src/comfy_mcp_server"]
# ComfyUI Registry configuration
[tool.comfy]
PublisherId = "dseditor"
DisplayName = "ComfyUI MCP Server"
Icon = ""
includes = []
# requires-comfyui = ">=1.0.0"
+5
View File
@@ -0,0 +1,5 @@
langchain>=0.3.19
langchain-ollama>=0.2.3
mcp[cli]>=1.2.1
Pillow>=10.0.0
hatchling
+203
View File
@@ -0,0 +1,203 @@
from mcp.server.fastmcp import FastMCP, Context
from mcp.types import ImageContent, TextContent
import json
import urllib
from urllib import request
import time
import os
import sys
import warnings
from io import BytesIO
from PIL import Image
# Suppress warnings to avoid polluting MCP stdout
warnings.filterwarnings("ignore")
# Ensure all logging goes to stderr, not stdout
import logging
logging.basicConfig(stream=sys.stderr, level=logging.WARNING)
from langchain_ollama.chat_models import ChatOllama
from langchain_core.prompts import PromptTemplate
from langchain_core.output_parsers import StrOutputParser
mcp = FastMCP("Comfy MCP Server")
host = os.environ.get("COMFY_URL")
override_host = os.environ.get("COMFY_URL_EXTERNAL")
if override_host is None:
override_host = host
workflow = os.environ.get("COMFY_WORKFLOW_JSON_FILE")
prompt_template = json.load(
open(workflow, "r")
) if workflow is not None else None
prompt_node_id = os.environ.get("PROMPT_NODE_ID")
output_node_id = os.environ.get("OUTPUT_NODE_ID")
output_mode = os.environ.get("OUTPUT_MODE")
# Configurable timeout settings
max_poll_attempts = int(os.environ.get("COMFY_MAX_POLL_ATTEMPTS", "60")) # Default 60 attempts
poll_interval = int(os.environ.get("COMFY_POLL_INTERVAL", "2")) # Default 2 seconds
ollama_api_base = os.environ.get("OLLAMA_API_BASE")
prompt_llm = os.environ.get("PROMPT_LLM")
def get_file_url(server: str, url_values: str) -> str:
return f"{server}/view?{url_values}"
if ollama_api_base is not None and prompt_llm is not None:
@mcp.tool()
def generate_prompt(topic: str, ctx: Context) -> str:
"""Write an image generation prompt for a provided topic"""
model = ChatOllama(base_url=ollama_api_base, model=prompt_llm)
prompt = PromptTemplate.from_template("""You are an AI Image Generation Prompt Assistant.
Your job is to review the topic provided by the user for an image generation task and create
an appropriate prompt from it. Repond with a single prompt. Don't ask for feedback about the prompt.
Topic: {topic}
Prompt: """)
chain = prompt | model | StrOutputParser()
response = chain.invoke({"topic": topic})
return response
@mcp.tool()
def generate_image(prompt: str, ctx: Context) -> list[TextContent | ImageContent]:
"""Generate an image using ComfyUI workflow"""
prompt_template[prompt_node_id]['inputs']['text'] = prompt
p = {"prompt": prompt_template}
data = json.dumps(p).encode('utf-8')
req = request.Request(f"{host}/prompt", data)
resp = request.urlopen(req)
response_ready = False
override_file_url = None
image_data = None
if resp.status == 200:
ctx.info("Submitted prompt")
resp_data = json.loads(resp.read())
prompt_id = resp_data["prompt_id"]
for t in range(0, max_poll_attempts):
history_req = request.Request(
f"{host}/history/{prompt_id}")
history_resp = request.urlopen(history_req)
if history_resp.status == 200:
ctx.info(f"Checking status... (attempt {t+1}/{max_poll_attempts})")
history_resp_data = json.loads(history_resp.read())
if prompt_id in history_resp_data:
status = (
history_resp_data[prompt_id]['status']['completed']
)
if status:
output_data = (
history_resp_data[prompt_id]
['outputs'][output_node_id]['images'][0]
)
url_values = urllib.parse.urlencode(output_data)
file_url = get_file_url(host, url_values)
override_file_url = get_file_url(
override_host, url_values)
file_req = request.Request(file_url)
file_resp = request.urlopen(file_req)
if file_resp.status == 200:
ctx.info("Image generated")
image_data = file_resp.read()
response_ready = True
break
else:
time.sleep(poll_interval)
else:
time.sleep(poll_interval)
if response_ready and override_file_url and image_data:
import base64
# Save image locally for Claude Code to access
local_save_dir = os.environ.get("COMFY_LOCAL_SAVE_DIR", "./generated_images")
os.makedirs(local_save_dir, exist_ok=True)
# Generate filename with timestamp
from datetime import datetime
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
# Convert PNG to WebP for smaller file size
try:
# Open the PNG image
png_image = Image.open(BytesIO(image_data))
# Save as WebP locally
local_filename = f"image_{timestamp}.webp"
local_path = os.path.abspath(os.path.join(local_save_dir, local_filename))
png_image.save(local_path, format='WebP', quality=85, method=6)
# Calculate size reduction
original_size_kb = len(image_data) / 1024
webp_size_kb = os.path.getsize(local_path) / 1024
reduction_percent = ((original_size_kb - webp_size_kb) / original_size_kb) * 100
ctx.info(f"Saved to {local_path}: {original_size_kb:.1f}KB -> {webp_size_kb:.1f}KB ({reduction_percent:.1f}% reduction)")
return [
TextContent(
type="text",
text=f"Image generated successfully!\n\n"
f"Remote URL: {override_file_url}\n"
f"Local Path: {local_path}\n"
f"Size: {original_size_kb:.1f}KB (PNG) -> {webp_size_kb:.1f}KB (WebP)\n"
f"Compression: {reduction_percent:.1f}% smaller\n\n"
f"You can read the image from the local path using the Read tool."
)
]
except Exception as e:
ctx.error(f"WebP conversion failed: {e}, falling back to PNG")
# Fallback to PNG if WebP conversion fails
local_filename = f"image_{timestamp}.png"
local_path = os.path.abspath(os.path.join(local_save_dir, local_filename))
with open(local_path, 'wb') as f:
f.write(image_data)
image_size_kb = len(image_data) / 1024
return [
TextContent(
type="text",
text=f"Image generated successfully!\n\n"
f"Remote URL: {override_file_url}\n"
f"Local Path: {local_path}\n"
f"Size: {image_size_kb:.1f}KB (PNG)\n\n"
f"You can read the image from the local path using the Read tool."
)
]
else:
return [TextContent(type="text", text="Failed to generate image. Please check server logs.")]
def run_server():
errors = []
if host is None:
errors.append("- COMFY_URL environment variable not set")
if workflow is None:
errors.append(
"- COMFY_WORKFLOW_JSON_FILE environment variable not set")
if prompt_node_id is None:
errors.append("- PROMPT_NODE_ID environment variable not set")
if output_node_id is None:
errors.append("- OUTPUT_NODE_ID environment variable not set")
if len(errors) > 0:
import sys
errors = ["Failed to start Comfy MCP Server:"] + errors
sys.stderr.write("\n".join(errors) + "\n")
sys.exit(1)
else:
mcp.run()
if __name__ == "__main__":
run_server()
+4
View File
@@ -0,0 +1,4 @@
from comfy_mcp_server import run_server
if __name__ == "__main__":
run_server()
Generated
+1507
View File
File diff suppressed because it is too large Load Diff
+140
View File
@@ -0,0 +1,140 @@
{
"9": {
"inputs": {
"filename_prefix": "z-image",
"images": [
"43",
0
]
},
"class_type": "SaveImage",
"_meta": {
"title": "Save Image"
}
},
"39": {
"inputs": {
"clip_name": "qwen_3_4b.safetensors",
"type": "lumina2",
"device": "default"
},
"class_type": "CLIPLoader",
"_meta": {
"title": "Load CLIP"
}
},
"40": {
"inputs": {
"vae_name": "ae.safetensors"
},
"class_type": "VAELoader",
"_meta": {
"title": "Load VAE"
}
},
"41": {
"inputs": {
"width": 1024,
"height": 1024,
"batch_size": 1
},
"class_type": "EmptySD3LatentImage",
"_meta": {
"title": "EmptySD3LatentImage"
}
},
"42": {
"inputs": {
"conditioning": [
"45",
0
]
},
"class_type": "ConditioningZeroOut",
"_meta": {
"title": "ConditioningZeroOut"
}
},
"43": {
"inputs": {
"samples": [
"44",
0
],
"vae": [
"40",
0
]
},
"class_type": "VAEDecode",
"_meta": {
"title": "VAE Decode"
}
},
"44": {
"inputs": {
"seed": 410513707389275,
"steps": 9,
"cfg": 1,
"sampler_name": "res_multistep",
"scheduler": "simple",
"denoise": 1,
"model": [
"47",
0
],
"positive": [
"45",
0
],
"negative": [
"42",
0
],
"latent_image": [
"41",
0
]
},
"class_type": "KSampler",
"_meta": {
"title": "KSampler"
}
},
"45": {
"inputs": {
"text": "Latina female with thick wavy hair, harbor boats and pastel houses behind. Breezy seaside light, warm tones, cinematic close-up.",
"clip": [
"39",
0
]
},
"class_type": "CLIPTextEncode",
"_meta": {
"title": "CLIP Text Encode (Prompt)"
}
},
"46": {
"inputs": {
"unet_name": "z-image-turbo_fp8_scaled_e4m3fn_KJ.safetensors",
"weight_dtype": "default"
},
"class_type": "UNETLoader",
"_meta": {
"title": "Load Diffusion Model"
}
},
"47": {
"inputs": {
"shift": 3,
"model": [
"46",
0
]
},
"class_type": "ModelSamplingAuraFlow",
"_meta": {
"title": "ModelSamplingAuraFlow"
}
}
}