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),
111 lines (110 loc) • 5.3 kB
JavaScript
// One-shot AI metadata proposer for Model Explorer's "Ask AI".
//
// Runs a SINGLE Claude Agent-SDK turn (no tools, no session) to distill the raw,
// messy metadata scraped from a .safetensors + Civitai into clean, prompt-ready
// structured fields. It reuses the orchestrator's Claude auth (same as the panel
// chat) — the AI hub — but is a discrete structured call, not the per-tab
// conversational agent (that "shared-context loop" is a larger, later rewrite).
//
// Returns a proposal the user REVIEWS and saves; nothing is written here.
import { loadQuery } from "./claude-backend.js";
import { logger } from "../utils/logger.js";
import { resolvePrompt } from "../services/prompt-overrides.js";
import { buildAgentSpawnEnv } from "../services/panel-secrets.js";
export const SYSTEM = `You are a metadata curator for AI image/video model files (LoRAs, checkpoints, VAEs, etc.) used in ComfyUI. Given raw, messy metadata scraped from a .safetensors file and Civitai, produce CLEAN, concise, prompt-ready structured metadata.
Rules:
- Output ONLY a single JSON object. No prose, no markdown fences.
- Distill marketing fluff into a tight, factual semantic_intent (1-2 sentences).
- trigger_tokens: the EXACT activation tokens (short), deduped. NEVER invent tokens not evidenced by trained tags / trainedWords / example prompts.
- prompt_guidance: 1-3 practical sentences on how to prompt this model well.
- Suggest default_strength_model / default_strength_clip and a safe strength_min/max ONLY when the evidence supports it (e.g. a weight like <lora:name:0.8> appears in an example prompt). Otherwise OMIT those fields entirely — do not guess.
- tags: short lowercase tags. compatible_families: only if reasonably certain.
- Do NOT fabricate civitai ids, hashes, or any fact not present in the evidence.`;
const SCHEMA_HINT = `Return a JSON object. All fields optional — include ONLY what the evidence supports:
{
"display_name": string,
"semantic_intent": string,
"description_clean": string,
"prompt_guidance": string,
"preservation_guidance": string,
"trigger_tokens": string[],
"activation_phrases": string[],
"negative_tokens": string[],
"default_strength_model": number,
"default_strength_clip": number,
"strength_min": number,
"strength_max": number,
"tags": string[],
"compatible_families": string[]
}`;
function buildUserText(ev) {
const parts = [`FILE: ${ev.filename}`];
if (ev.classify)
parts.push(`CLASSIFY: ${JSON.stringify(ev.classify)}`);
if (ev.model_card && Object.keys(ev.model_card).length)
parts.push(`CURRENT model_card: ${JSON.stringify(ev.model_card)}`);
if (ev.prompt_director && Object.keys(ev.prompt_director).length)
parts.push(`CURRENT prompt_director: ${JSON.stringify(ev.prompt_director)}`);
if (ev.modelspec && Object.keys(ev.modelspec).length)
parts.push(`modelspec (read-only): ${JSON.stringify(ev.modelspec)}`);
if (ev.tag_frequency_top?.length)
parts.push(`TOP TRAINING TAGS (freq): ${JSON.stringify(ev.tag_frequency_top)}`);
if (ev.description)
parts.push(`CIVITAI DESCRIPTION (may be messy marketing):\n${ev.description.slice(0, 4000)}`);
if (ev.example_prompts?.length)
parts.push(`EXAMPLE PROMPTS:\n${ev.example_prompts.slice(0, 5).map((p, i) => `${i + 1}. ${p.slice(0, 500)}`).join("\n")}`);
parts.push(SCHEMA_HINT, "Return ONLY the JSON object.");
return parts.join("\n\n");
}
function extractJson(s) {
let t = s.replace(/```json\s*/gi, "").replace(/```/g, "").trim();
const start = t.indexOf("{");
const end = t.lastIndexOf("}");
if (start >= 0 && end > start)
t = t.slice(start, end + 1);
try {
const v = JSON.parse(t);
return v && typeof v === "object" ? v : null;
}
catch {
return null;
}
}
export async function proposeModelCard(ev, model) {
const query = await loadQuery();
const q = query({
prompt: buildUserText(ev),
options: {
...(model ? { model } : {}),
systemPrompt: resolvePrompt("proposer.modelCard", SYSTEM),
permissionMode: "bypassPermissions",
allowDangerouslySkipPermissions: true,
allowedTools: [],
strictMcpConfig: true,
maxTurns: 1,
// SECURITY: the SDK spawns a Claude Code subprocess; `env` REPLACES its
// environment entirely. Pass the agent env (process.env MINUS tool-only
// secrets) so RunPod/HF/CivitAI tokens never reach the LLM subprocess.
env: buildAgentSpawnEnv(),
},
});
let text = "";
let result = "";
for await (const m of q) {
const mm = m;
if (mm?.type === "assistant" && mm.message?.content) {
for (const b of mm.message.content)
if (b?.type === "text" && b.text)
text += b.text;
}
else if (mm?.type === "result" && typeof mm.result === "string") {
result = mm.result;
}
}
const raw = (result || text).trim();
const proposal = extractJson(raw);
if (!proposal)
logger.warn(`[ai-proposer] no JSON parsed from model reply (${raw.length} chars)`);
return { proposal, raw };
}
//# sourceMappingURL=ai-proposer.js.map