Skip to content

Commit 34042d4

Browse files
CopilotJohn Dhybridindie
authored
feat: add get_model_presets and get_prompting_guide discovery tools (#25)
* feat: add model presets and prompting guide discovery tools Closes #18 - Add get_model_presets(model_name|model_family) with family normalization, filename-based inference, and curated recommended generation settings. - Add get_prompting_guide(model_family) with model-family prompt structure, weighting guidance, quality tags, and negative prompt tips. - Apply read limiter and audit logging to both tools. - Add tests for family lookup, inference, validation, and guide retrieval. - Document new tools in README discovery table. * Initial plan * feat: add get_model_presets and get_prompting_guide discovery tools Closes #18 - Add get_model_presets(model_name|model_family): family normalization, filename-based inference, and curated recommended generation settings. - Add get_prompting_guide(model_family): prompt structure, weight syntax, quality tags, and negative prompt tips per model family. - Apply read limiter and audit logging to both tools. - Removed redundant self-mapping aliases (sdxl, flux) from alias table. - Explicit ValueError when model filename is unrecognized (None guard). - 6 new tests covering family lookup, inference, validation, guide retrieval, unknown-family rejection, and unrecognized filename rejection. - Document new tools in README discovery table. Co-authored-by: hybridindie <20465+hybridindie@users.noreply.github.com> * refactor: remove remaining redundant self-mapping aliases (sd3) Co-authored-by: hybridindie <20465+hybridindie@users.noreply.github.com> --------- Co-authored-by: John D <johnd@localhost> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: hybridindie <20465+hybridindie@users.noreply.github.com>
1 parent a11ee18 commit 34042d4

File tree

3 files changed

+257
-0
lines changed

3 files changed

+257
-0
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,8 @@ docker run --rm ghcr.io/hybridindie/comfyui-mcp:latest --help
177177
| `download_model` | Download a model via [ComfyUI-Model-Manager](https://github.com/hayden-fr/ComfyUI-Model-Manager). URL and extension validated. |
178178
| `get_download_tasks` | Check status of active model downloads (progress, speed, status). |
179179
| `cancel_download` | Cancel or clean up a model download task. |
180+
| `get_model_presets` | Return recommended sampler/scheduler/steps/CFG defaults for a model family. |
181+
| `get_prompting_guide` | Return model-family prompt engineering tips and negative prompt guidance. |
180182

181183
> **Requires:** [ComfyUI-Model-Manager](https://github.com/hayden-fr/ComfyUI-Model-Manager) installed in your ComfyUI instance. Download tools are gated behind lazy detection — if Model Manager is not installed, these tools return a helpful error message. `search_models` works without it.
182184

src/comfyui_mcp/tools/discovery.py

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,136 @@
1212
from comfyui_mcp.security.rate_limit import RateLimiter
1313
from comfyui_mcp.security.sanitizer import PathSanitizer
1414

15+
_SUPPORTED_MODEL_FAMILIES = {"sd15", "sdxl", "flux", "sd3", "cascade"}
16+
17+
_MODEL_FAMILY_ALIASES = {
18+
"sd1.5": "sd15",
19+
"sd 1.5": "sd15",
20+
"stable-diffusion-1.5": "sd15",
21+
"stable diffusion 1.5": "sd15",
22+
"stable_diffusion_1_5": "sd15",
23+
"stable-diffusion-xl": "sdxl",
24+
"stable diffusion xl": "sdxl",
25+
"stable_diffusion_xl": "sdxl",
26+
"flux.1": "flux",
27+
"sd3.5": "sd3",
28+
"stable-cascade": "cascade",
29+
}
30+
31+
_MODEL_PRESETS: dict[str, dict[str, Any]] = {
32+
"sd15": {
33+
"recommended": {
34+
"sampler": "euler_ancestral",
35+
"scheduler": "normal",
36+
"steps": 28,
37+
"cfg": 7.0,
38+
"resolution": "512x768",
39+
"clip_skip": 1,
40+
"notes": "Tag-heavy prompts and negative prompts work well.",
41+
}
42+
},
43+
"sdxl": {
44+
"recommended": {
45+
"sampler": "dpmpp_2m",
46+
"scheduler": "karras",
47+
"steps": 30,
48+
"cfg": 5.5,
49+
"resolution": "1024x1024",
50+
"clip_skip": 1,
51+
"notes": "Prefer natural language prompts with clear scene composition.",
52+
}
53+
},
54+
"flux": {
55+
"recommended": {
56+
"sampler": "euler",
57+
"scheduler": "simple",
58+
"steps": 20,
59+
"cfg": 1.0,
60+
"resolution": "1024x1024",
61+
"clip_skip": 1,
62+
"notes": "Flow-matching models expect low CFG and concise language.",
63+
}
64+
},
65+
"sd3": {
66+
"recommended": {
67+
"sampler": "dpmpp_2m",
68+
"scheduler": "sgm_uniform",
69+
"steps": 28,
70+
"cfg": 4.5,
71+
"resolution": "1024x1024",
72+
"clip_skip": 1,
73+
"notes": "Use detailed, descriptive prompts; avoid over-weighting terms.",
74+
}
75+
},
76+
"cascade": {
77+
"recommended": {
78+
"sampler": "dpmpp_2m",
79+
"scheduler": "simple",
80+
"steps": 24,
81+
"cfg": 4.0,
82+
"resolution": "1024x1024",
83+
"clip_skip": 1,
84+
"notes": "Cascade benefits from broad composition instructions first.",
85+
}
86+
},
87+
}
88+
89+
_PROMPTING_GUIDES: dict[str, dict[str, Any]] = {
90+
"sd15": {
91+
"prompt_structure": "subject, style, lighting, lens/composition, quality tags",
92+
"weight_syntax": "(token:1.2)",
93+
"quality_tags": ["masterpiece", "best quality", "high detail"],
94+
"negative_prompt_tips": "Use negatives for anatomy artifacts and low-quality tokens.",
95+
},
96+
"sdxl": {
97+
"prompt_structure": "subject + environment + mood + camera framing",
98+
"weight_syntax": "(token:1.1)",
99+
"quality_tags": ["cinematic lighting", "high detail", "sharp focus"],
100+
"negative_prompt_tips": "Keep negatives shorter than SD1.5 to avoid over-constraining.",
101+
},
102+
"flux": {
103+
"prompt_structure": "natural language sentence describing subject, setting, and style",
104+
"weight_syntax": "Avoid heavy weighting unless necessary",
105+
"quality_tags": ["natural lighting", "detailed texture"],
106+
"negative_prompt_tips": "Use short negatives only for hard constraints (e.g. watermark).",
107+
},
108+
"sd3": {
109+
"prompt_structure": "clear scene description with explicit style and camera intent",
110+
"weight_syntax": "Light weighting only; rely on plain language first",
111+
"quality_tags": ["balanced composition", "fine detail"],
112+
"negative_prompt_tips": (
113+
"Use focused negatives for specific defects, not long keyword lists."
114+
),
115+
},
116+
"cascade": {
117+
"prompt_structure": "high-level composition first, then style modifiers",
118+
"weight_syntax": "(token:1.1) for minor emphasis",
119+
"quality_tags": ["clean composition", "color harmony"],
120+
"negative_prompt_tips": "Keep negatives concise; tune guidance before adding many tokens.",
121+
},
122+
}
123+
124+
125+
def _normalize_model_family(model_family: str) -> str:
126+
key = model_family.strip().lower()
127+
return _MODEL_FAMILY_ALIASES.get(key, key)
128+
129+
130+
def _infer_model_family(model_name: str) -> str | None:
131+
name = model_name.strip().lower()
132+
checks = [
133+
("flux", "flux"),
134+
("sdxl", "sdxl"),
135+
("sd3", "sd3"),
136+
("cascade", "cascade"),
137+
("dreamshaper", "sd15"),
138+
("anything", "sd15"),
139+
]
140+
for needle, family in checks:
141+
if needle in name:
142+
return family
143+
return None
144+
15145

16146
def register_discovery_tools(
17147
mcp: FastMCP,
@@ -208,4 +338,72 @@ async def get_system_info() -> dict:
208338

209339
tool_fns["get_system_info"] = get_system_info
210340

341+
@mcp.tool()
342+
async def get_model_presets(
343+
model_name: str | None = None,
344+
model_family: str | None = None,
345+
) -> dict[str, Any]:
346+
"""Return recommended generation presets for a model family.
347+
348+
Args:
349+
model_name: Optional model filename to infer family from.
350+
model_family: Optional explicit family (sd15, sdxl, flux, sd3, cascade).
351+
352+
Returns:
353+
Dictionary containing normalized family and recommended settings.
354+
"""
355+
limiter.check("get_model_presets")
356+
audit.log(
357+
tool="get_model_presets",
358+
action="called",
359+
extra={"model_name": model_name, "model_family": model_family},
360+
)
361+
362+
family: str | None = None
363+
if model_family:
364+
family = _normalize_model_family(model_family)
365+
elif model_name:
366+
family = _infer_model_family(model_name)
367+
if family is None:
368+
raise ValueError(f"Could not infer model family from: {model_name}")
369+
else:
370+
raise ValueError("Provide either model_name or model_family")
371+
372+
if family not in _SUPPORTED_MODEL_FAMILIES:
373+
supported = ", ".join(sorted(_SUPPORTED_MODEL_FAMILIES))
374+
raise ValueError(f"Unknown model family: {family}. Supported families: {supported}")
375+
376+
return {
377+
"family": family,
378+
**_MODEL_PRESETS[family],
379+
}
380+
381+
tool_fns["get_model_presets"] = get_model_presets
382+
383+
@mcp.tool()
384+
async def get_prompting_guide(model_family: str) -> dict[str, Any]:
385+
"""Return prompt-engineering guidance for a model family.
386+
387+
Args:
388+
model_family: Family name (sd15, sdxl, flux, sd3, cascade).
389+
"""
390+
limiter.check("get_prompting_guide")
391+
normalized = _normalize_model_family(model_family)
392+
audit.log(
393+
tool="get_prompting_guide",
394+
action="called",
395+
extra={"model_family": normalized},
396+
)
397+
398+
if normalized not in _SUPPORTED_MODEL_FAMILIES:
399+
supported = ", ".join(sorted(_SUPPORTED_MODEL_FAMILIES))
400+
raise ValueError(f"Unknown model family: {normalized}. Supported families: {supported}")
401+
402+
return {
403+
"family": normalized,
404+
"guide": _PROMPTING_GUIDES[normalized],
405+
}
406+
407+
tool_fns["get_prompting_guide"] = get_prompting_guide
408+
211409
return tool_fns

tests/test_tools_discovery.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,3 +304,60 @@ async def test_rate_limit_enforced(self, tmp_path):
304304

305305
with pytest.raises(RateLimitError):
306306
await tools["get_system_info"]()
307+
308+
309+
class TestModelPresetsAndGuides:
310+
async def test_get_model_presets_by_family(self, components):
311+
client, audit, limiter, sanitizer = components
312+
mcp = FastMCP("test")
313+
tools = register_discovery_tools(mcp, client, audit, limiter, sanitizer)
314+
315+
result = await tools["get_model_presets"](model_family="flux")
316+
317+
assert result["family"] == "flux"
318+
assert result["recommended"]["sampler"] == "euler"
319+
assert result["recommended"]["cfg"] == 1.0
320+
321+
async def test_get_model_presets_infers_family_from_model_name(self, components):
322+
client, audit, limiter, sanitizer = components
323+
mcp = FastMCP("test")
324+
tools = register_discovery_tools(mcp, client, audit, limiter, sanitizer)
325+
326+
result = await tools["get_model_presets"](model_name="flux1-dev-fp8.safetensors")
327+
328+
assert result["family"] == "flux"
329+
330+
async def test_get_model_presets_rejects_missing_inputs(self, components):
331+
client, audit, limiter, sanitizer = components
332+
mcp = FastMCP("test")
333+
tools = register_discovery_tools(mcp, client, audit, limiter, sanitizer)
334+
335+
with pytest.raises(ValueError, match="Provide either model_name or model_family"):
336+
await tools["get_model_presets"]()
337+
338+
async def test_get_prompting_guide_returns_data(self, components):
339+
client, audit, limiter, sanitizer = components
340+
mcp = FastMCP("test")
341+
tools = register_discovery_tools(mcp, client, audit, limiter, sanitizer)
342+
343+
result = await tools["get_prompting_guide"]("sdxl")
344+
345+
assert result["family"] == "sdxl"
346+
assert "prompt_structure" in result["guide"]
347+
assert "negative_prompt_tips" in result["guide"]
348+
349+
async def test_get_prompting_guide_rejects_unknown_family(self, components):
350+
client, audit, limiter, sanitizer = components
351+
mcp = FastMCP("test")
352+
tools = register_discovery_tools(mcp, client, audit, limiter, sanitizer)
353+
354+
with pytest.raises(ValueError, match="Unknown model family"):
355+
await tools["get_prompting_guide"]("unknown")
356+
357+
async def test_get_model_presets_rejects_unrecognized_model_name(self, components):
358+
client, audit, limiter, sanitizer = components
359+
mcp = FastMCP("test")
360+
tools = register_discovery_tools(mcp, client, audit, limiter, sanitizer)
361+
362+
with pytest.raises(ValueError, match="Could not infer model family from"):
363+
await tools["get_model_presets"](model_name="mystery_model_v1.safetensors")

0 commit comments

Comments
 (0)