comfyui-mcp
Version:
Local-first, agent-native control plane for ComfyUI — MCP server + autonomous sidebar agent that drives your live graph in natural language on ANY LLM: Claude/ChatGPT/Gemini on your subscription (no API key), free local models via Ollama (fully offline),
165 lines (143 loc) • 6.1 kB
JavaScript
import fs from 'fs';
import path from 'path';
const PLUGIN_DIR = 'plugin';
const AGENTS_DIR = '.agents';
const GEMINI_DIR = '.gemini/commands';
// Ensure directories exist
fs.mkdirSync(path.join(AGENTS_DIR, 'skills'), { recursive: true });
fs.mkdirSync(GEMINI_DIR, { recursive: true });
function extractFrontmatter(content) {
// Normalize BOM + CRLF before matching the fence, the same way splitFrontmatter
// (src/tools/skills-access.ts) does. Without this, every checkout with
// core.autocrlf=true — the Windows default — fails `^---\n`, falls through to the
// no-frontmatter branch, and generates .agents/skills/<n>/SKILL.md with an EMPTY
// description plus the original frontmatter duplicated into the body. That was true
// for all 39 skills. Same root cause as #1617, which fixed a test that read raw bytes;
// this is the generator that had the identical bug.
const normalized = content.replace(/^/, '').replace(/\r\n/g, '\n');
const match = normalized.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
if (!match) return { frontmatter: {}, body: normalized };
const frontmatterStr = match[1];
const body = match[2];
const frontmatter = {};
for (const line of frontmatterStr.split('\n')) {
const colonIdx = line.indexOf(':');
if (colonIdx !== -1) {
const key = line.slice(0, colonIdx).trim();
const value = line.slice(colonIdx + 1).trim();
if (!key.startsWith('globs') && !line.trim().startsWith('-')) {
frontmatter[key] = value.replace(/^['"]|['"]$/g, '');
}
}
}
return { frontmatter, body };
}
function processSkillOrAgent(srcPath, destPath, defaultName, isAgent) {
if (!fs.existsSync(srcPath)) return;
const content = fs.readFileSync(srcPath, 'utf8');
let { frontmatter, body } = extractFrontmatter(content);
const name = frontmatter.name || defaultName;
const description = frontmatter.description || (isAgent ? `Agent specialized in ${name}` : '');
// Replace CLAUDE_PLUGIN_ROOT with ./plugin
body = body.replace(/\$\{CLAUDE_PLUGIN_ROOT\}/g, './plugin');
const newContent = `---
name: ${name}
description: ${description}
---
${body.trim()}`;
fs.mkdirSync(path.dirname(destPath), { recursive: true });
fs.writeFileSync(destPath, newContent, 'utf8');
console.log(`Synced ${srcPath} -> ${destPath}`);
}
// 1. Translate skills
const skillsDir = path.join(PLUGIN_DIR, 'skills');
if (fs.existsSync(skillsDir)) {
for (const skillName of fs.readdirSync(skillsDir)) {
const skillSrcDir = path.join(skillsDir, skillName);
if (!fs.statSync(skillSrcDir).isDirectory()) continue;
const srcPath = path.join(skillSrcDir, 'SKILL.md');
const destSkillDir = path.join(AGENTS_DIR, 'skills', skillName);
const destPath = path.join(destSkillDir, 'SKILL.md');
processSkillOrAgent(srcPath, destPath, skillName, false);
// Also copy sibling asset folders the SKILL.md links to (references/, docs/).
// Without this the synced bundle keeps the SKILL.md but drops the docs it
// points at, leaving dangling relative-doc links in .agents/skills/<name>/
// (the class of breakage reported in #552).
for (const sub of ['references', 'docs']) {
const subSrc = path.join(skillSrcDir, sub);
if (fs.existsSync(subSrc) && fs.statSync(subSrc).isDirectory()) {
const subDest = path.join(destSkillDir, sub);
fs.cpSync(subSrc, subDest, { recursive: true });
console.log(`Synced ${subSrc} -> ${subDest}`);
}
}
}
}
// 2. Translate agents
const agentsDir = path.join(PLUGIN_DIR, 'agents');
if (fs.existsSync(agentsDir)) {
for (const file of fs.readdirSync(agentsDir)) {
if (file.endsWith('.md')) {
const agentName = path.basename(file, '.md');
const srcPath = path.join(agentsDir, file);
const destPath = path.join(AGENTS_DIR, 'skills', `comfy-${agentName}`, 'SKILL.md');
processSkillOrAgent(srcPath, destPath, `comfy-${agentName}`, true);
}
}
}
// 3. Translate commands
const commandsDir = path.join(PLUGIN_DIR, 'commands');
if (fs.existsSync(commandsDir)) {
for (const file of fs.readdirSync(commandsDir)) {
if (file.endsWith('.md')) {
const commandName = path.basename(file, '.md');
const srcPath = path.join(commandsDir, file);
const destPath = path.join(GEMINI_DIR, `comfy-${commandName}.toml`);
const content = fs.readFileSync(srcPath, 'utf8');
let { frontmatter, body } = extractFrontmatter(content);
body = body.replace(/\$ARGUMENTS/g, '{{args}}');
body = body.replace(/\$\{CLAUDE_PLUGIN_ROOT\}/g, './plugin');
const desc = frontmatter.description || `Command comfy-${commandName}`;
const tomlContent = `name = "comfy-${commandName}"
description = "${desc}"
prompt = """
${body.trim()}
"""
`;
fs.writeFileSync(destPath, tomlContent, 'utf8');
console.log(`Synced ${srcPath} -> ${destPath}`);
}
}
}
// 4. Translate hooks
const hooksSrc = path.join(PLUGIN_DIR, 'hooks', 'hooks.json');
const hooksDest = path.join(AGENTS_DIR, 'hooks.json');
if (fs.existsSync(hooksSrc)) {
let hooksContent = fs.readFileSync(hooksSrc, 'utf8');
hooksContent = hooksContent.replace(/mcp__plugin_comfy_comfyui__/g, 'mcp__comfyui__');
hooksContent = hooksContent.replace(/\$\{CLAUDE_PLUGIN_ROOT\}\/hooks/g, './plugin/hooks');
hooksContent = hooksContent.replace(/\$\{CLAUDE_PLUGIN_ROOT\}\/scripts/g, './plugin/scripts');
fs.writeFileSync(hooksDest, hooksContent, 'utf8');
console.log(`Synced hooks -> ${hooksDest}`);
}
// 5. Create default mcp_config.json
const mcpConfigPath = path.join(AGENTS_DIR, 'mcp_config.json');
const mcpConfig = {
mcpServers: {
comfyui: {
command: "npx",
args: ["-y", "comfyui-mcp"],
env: {
CIVITAI_API_TOKEN: ""
}
},
huggingface: {
url: "https://huggingface.co/mcp",
headers: {
Authorization: "Bearer ${HUGGINGFACE_TOKEN:-}"
}
}
}
};
fs.writeFileSync(mcpConfigPath, JSON.stringify(mcpConfig, null, 2), 'utf8');
console.log(`Created default ${mcpConfigPath}`);