RioShiina commited on
Commit
46bcda5
·
verified ·
1 Parent(s): 029170e

Optimize API/MCP Tools.

Browse files
core/pipelines/pipeline_input_processor.py CHANGED
@@ -31,8 +31,11 @@ def process_pipeline_inputs(ui_inputs: Dict[str, Any], progress: gr.Progress, wo
31
  if input_image_pil:
32
  img_w, img_h = input_image_pil.width, input_image_pil.height
33
  elif task_type == 'inpaint':
 
34
  inpaint_dict = ui_inputs.get('inpaint_image_dict')
35
- if inpaint_dict and inpaint_dict.get('background'):
 
 
36
  img_w, img_h = inpaint_dict['background'].width, inpaint_dict['background'].height
37
  elif task_type == 'outpaint':
38
  input_image_pil = ui_inputs.get('outpaint_image')
@@ -88,27 +91,36 @@ def process_pipeline_inputs(ui_inputs: Dict[str, Any], progress: gr.Progress, wo
88
  ui_inputs['height'] = input_image_pil.height
89
 
90
  elif task_type == 'inpaint':
 
91
  inpaint_dict = ui_inputs.get('inpaint_image_dict')
92
- if not inpaint_dict or not inpaint_dict.get('background') or not inpaint_dict.get('layers'):
93
- raise gr.Error("Inpainting requires an input image and a drawn mask.")
94
-
95
- background_img = inpaint_dict['background'].convert("RGBA")
96
- composite_mask_pil = Image.new('L', background_img.size, 0)
97
- for layer in inpaint_dict['layers']:
98
- if layer:
99
- layer_alpha = layer.split()[-1]
100
- composite_mask_pil = ImageChops.lighter(composite_mask_pil, layer_alpha)
101
 
102
- inverted_mask_alpha = Image.fromarray(255 - np.array(composite_mask_pil), mode='L')
103
- r, g, b, _ = background_img.split()
104
- composite_image_with_mask = Image.merge('RGBA', [r, g, b, inverted_mask_alpha])
105
-
106
- temp_file_path = os.path.join(INPUT_DIR, f"temp_inpaint_composite_{random.randint(1000, 9999)}.png")
107
- composite_image_with_mask.save(temp_file_path, "PNG")
108
-
109
- ui_inputs['input_image'] = os.path.basename(temp_file_path)
110
- temp_files_to_clean.append(temp_file_path)
111
- ui_inputs.pop('inpaint_mask', None)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
112
 
113
  elif task_type == 'outpaint':
114
  input_image_pil = ui_inputs.get('outpaint_image')
@@ -150,13 +162,6 @@ def process_pipeline_inputs(ui_inputs: Dict[str, Any], progress: gr.Progress, wo
150
 
151
  if emb_filename:
152
  embedding_filenames.append(emb_filename)
153
-
154
- if embedding_filenames:
155
- embedding_prompt_text = " ".join([f"embedding:{f}" for f in embedding_filenames])
156
- if ui_inputs['positive_prompt']:
157
- ui_inputs['positive_prompt'] = f"{ui_inputs['positive_prompt']}, {embedding_prompt_text}"
158
- else:
159
- ui_inputs['positive_prompt'] = embedding_prompt_text
160
 
161
  controlnet_data = ui_inputs.get('controlnet_data', [])
162
  active_controlnets = []
 
31
  if input_image_pil:
32
  img_w, img_h = input_image_pil.width, input_image_pil.height
33
  elif task_type == 'inpaint':
34
+ inpaint_img = ui_inputs.get('inpaint_image')
35
  inpaint_dict = ui_inputs.get('inpaint_image_dict')
36
+ if inpaint_img:
37
+ img_w, img_h = inpaint_img.width, inpaint_img.height
38
+ elif inpaint_dict and inpaint_dict.get('background'):
39
  img_w, img_h = inpaint_dict['background'].width, inpaint_dict['background'].height
40
  elif task_type == 'outpaint':
41
  input_image_pil = ui_inputs.get('outpaint_image')
 
91
  ui_inputs['height'] = input_image_pil.height
92
 
93
  elif task_type == 'inpaint':
94
+ inpaint_img = ui_inputs.get('inpaint_image')
95
  inpaint_dict = ui_inputs.get('inpaint_image_dict')
 
 
 
 
 
 
 
 
 
96
 
97
+ if inpaint_img:
98
+ temp_file_path = os.path.join(INPUT_DIR, f"temp_inpaint_{random.randint(1000, 9999)}.png")
99
+ inpaint_img.save(temp_file_path, "PNG")
100
+ ui_inputs['input_image'] = os.path.basename(temp_file_path)
101
+ temp_files_to_clean.append(temp_file_path)
102
+ ui_inputs['width'] = inpaint_img.width
103
+ ui_inputs['height'] = inpaint_img.height
104
+ elif inpaint_dict and inpaint_dict.get('background') and inpaint_dict.get('layers'):
105
+ background_img = inpaint_dict['background'].convert("RGBA")
106
+ composite_mask_pil = Image.new('L', background_img.size, 0)
107
+ for layer in inpaint_dict['layers']:
108
+ if layer:
109
+ layer_alpha = layer.split()[-1]
110
+ composite_mask_pil = ImageChops.lighter(composite_mask_pil, layer_alpha)
111
+
112
+ inverted_mask_alpha = Image.fromarray(255 - np.array(composite_mask_pil), mode='L')
113
+ r, g, b, _ = background_img.split()
114
+ composite_image_with_mask = Image.merge('RGBA', [r, g, b, inverted_mask_alpha])
115
+
116
+ temp_file_path = os.path.join(INPUT_DIR, f"temp_inpaint_composite_{random.randint(1000, 9999)}.png")
117
+ composite_image_with_mask.save(temp_file_path, "PNG")
118
+
119
+ ui_inputs['input_image'] = os.path.basename(temp_file_path)
120
+ temp_files_to_clean.append(temp_file_path)
121
+ ui_inputs.pop('inpaint_mask', None)
122
+ else:
123
+ raise gr.Error("Inpainting requires an input image with a mask.")
124
 
125
  elif task_type == 'outpaint':
126
  input_image_pil = ui_inputs.get('outpaint_image')
 
162
 
163
  if emb_filename:
164
  embedding_filenames.append(emb_filename)
 
 
 
 
 
 
 
165
 
166
  controlnet_data = ui_inputs.get('controlnet_data', [])
167
  active_controlnets = []
mcp_tools/__init__.py CHANGED
@@ -8,8 +8,7 @@ from .get_model_architecture_list import handle_get_model_architecture_list
8
  from .get_model_list import handle_get_model_list
9
  from .get_feature_list import handle_get_feature_list
10
  from .get_model_features import handle_get_model_features
11
- from .get_chain_schema import handle_get_chain_schema
12
- from .run_imagegen import handle_run_imagegen
13
  from .get_task_status import handle_get_task_status
14
  from .error_schema import make_error, make_validation_error, make_not_found_error
15
  from .mcp_gradio_integration import (
@@ -25,9 +24,8 @@ MCP_FUNCTIONS = [
25
  handle_get_model_list,
26
  handle_get_feature_list,
27
  handle_get_model_features,
28
- handle_run_imagegen,
29
  handle_get_task_status,
30
- handle_get_chain_schema,
31
  ]
32
 
33
  __all__ = [
@@ -36,8 +34,7 @@ __all__ = [
36
  "handle_get_model_list",
37
  "handle_get_feature_list",
38
  "handle_get_model_features",
39
- "handle_get_chain_schema",
40
- "handle_run_imagegen",
41
  "handle_get_task_status",
42
  "make_error",
43
  "make_validation_error",
 
8
  from .get_model_list import handle_get_model_list
9
  from .get_feature_list import handle_get_feature_list
10
  from .get_model_features import handle_get_model_features
11
+ from .run import handle_run
 
12
  from .get_task_status import handle_get_task_status
13
  from .error_schema import make_error, make_validation_error, make_not_found_error
14
  from .mcp_gradio_integration import (
 
24
  handle_get_model_list,
25
  handle_get_feature_list,
26
  handle_get_model_features,
27
+ handle_run,
28
  handle_get_task_status,
 
29
  ]
30
 
31
  __all__ = [
 
34
  "handle_get_model_list",
35
  "handle_get_feature_list",
36
  "handle_get_model_features",
37
+ "handle_run",
 
38
  "handle_get_task_status",
39
  "make_error",
40
  "make_validation_error",
mcp_tools/common.py CHANGED
@@ -7,6 +7,7 @@ import os
7
  import time
8
  import urllib.parse
9
  import urllib.request
 
10
  import base64
11
  import io
12
  import yaml
@@ -24,8 +25,28 @@ _CHAIN_FEATURES_PATH = os.path.join(_YAML_DIR, "chain_features.yaml")
24
  _CONSTANTS_PATH = os.path.join(_YAML_DIR, "constants.yaml")
25
 
26
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
  def _parse_image_param(image_param: Any) -> Any:
28
- """Parse a Base64 Data URI, local file path, or PIL.Image into a PIL Image object. HTTP URLs are not supported."""
29
  if isinstance(image_param, Image.Image):
30
  return image_param
31
 
@@ -34,11 +55,9 @@ def _parse_image_param(image_param: Any) -> Any:
34
 
35
  image_param = image_param.strip()
36
 
37
- # Reject HTTP / HTTPS URL
38
  if image_param.startswith("http://") or image_param.startswith("https://"):
39
- raise ValueError(
40
- "Image URLs are not supported. Please supply the image directly as a Base64 Data URI (e.g., 'data:image/png;base64,...')."
41
- )
42
 
43
  # Base64 Data URI (e.g. data:image/png;base64,...)
44
  if image_param.startswith("data:image/"):
@@ -47,22 +66,74 @@ def _parse_image_param(image_param: Any) -> Any:
47
  return Image.open(io.BytesIO(data))
48
 
49
  # Base64 string without header
50
- if len(image_param) > 100 and not os.path.exists(image_param):
51
  try:
52
  data = base64.b64decode(image_param)
53
  return Image.open(io.BytesIO(data))
54
  except Exception:
55
  pass
56
 
57
- # Local file path
58
- if os.path.exists(image_param):
59
- return Image.open(image_param)
60
-
61
  raise ValueError(
62
- "Invalid image parameter format. Expected a Base64 Data URI (e.g., 'data:image/png;base64,...') or local file path."
 
63
  )
64
 
65
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
  def _load_yaml(filepath: str) -> dict:
67
  """Safely load a YAML file, returning an empty dict if the file does not exist."""
68
  if not os.path.exists(filepath):
@@ -111,7 +182,7 @@ _TASK_DEFINITIONS = [
111
  "display_name": "Hi-Res Fix / Upscale",
112
  "description": "Enhance details and upscale an existing low-resolution image.",
113
  "required_inputs": ["prompt", "image", "upscale_by"],
114
- "optional_inputs": _COMMON_OPTIONAL_INPUTS,
115
  },
116
  ]
117
 
@@ -226,48 +297,369 @@ def _execute_imagegen_pipeline(task_id: str, params: dict):
226
  ui_inputs["feathering"] = params.get("feathering", 10)
227
  elif task_type == "hires_fix":
228
  ui_inputs["hires_image"] = pil_img
229
- ui_inputs["hires_upscaler"] = params.get("upscaler", "latent")
 
 
 
230
  ui_inputs["hires_scale_by"] = params.get("upscale_by", 2.0)
231
  ui_inputs["hires_denoise"] = params.get("denoise", 0.55)
232
 
233
  chain = params.get("chain", [])
234
  if chain:
235
  lora_data = []
 
236
  controlnet_data = []
 
237
  ipadapter_data = []
238
- style_data = []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
239
 
240
  for item in chain:
241
  itype = item.get("injector_type")
242
  if itype == "lora":
243
  lora_data.extend([
244
- item.get("lora_source", "Civitai"),
245
  item.get("lora_value", ""),
246
  item.get("scale", 1.0),
247
  None
248
  ])
249
- elif itype in ("controlnet", "krea2_controlnet", "anima_controlnet_lllite"):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
250
  controlnet_data.extend([
251
- item.get("control_net_name", ""),
252
- _parse_image_param(item.get("image")),
253
- item.get("strength", 1.0)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
254
  ])
255
- elif itype in ("ipadapter", "flux1_ipadapter", "sd3_ipadapter"):
256
- ipadapter_data.extend([
257
- item.get("preset", "STANDARD (medium strength)"),
258
- _parse_image_param(item.get("image")),
259
- item.get("weight", 1.0)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
260
  ])
261
- elif itype == "style":
262
- style_data.extend([
263
- _parse_image_param(item.get("image")),
264
- item.get("strength", 1.0)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
265
  ])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
266
 
267
  if lora_data: ui_inputs["lora_data"] = lora_data
 
268
  if controlnet_data: ui_inputs["controlnet_data"] = controlnet_data
269
- if ipadapter_data: ui_inputs["ipadapter_data"] = ipadapter_data
270
- if style_data: ui_inputs["style_data"] = style_data
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
271
 
272
  _TASKS_DB[task_id]["progress"] = 50
273
 
 
7
  import time
8
  import urllib.parse
9
  import urllib.request
10
+ import urllib.error
11
  import base64
12
  import io
13
  import yaml
 
25
  _CONSTANTS_PATH = os.path.join(_YAML_DIR, "constants.yaml")
26
 
27
 
28
+ _MAX_IMAGE_DOWNLOAD_BYTES = 50 * 1024 * 1024 # 50 MB
29
+ _IMAGE_DOWNLOAD_TIMEOUT = 30 # seconds
30
+ _ALLOWED_IMAGE_CONTENT_TYPES = frozenset([
31
+ "image/png", "image/jpeg", "image/jpg", "image/gif",
32
+ "image/webp", "image/bmp", "image/tiff",
33
+ ])
34
+
35
+
36
+ def _get_ipadapter_presets_by_arch() -> Dict[str, list]:
37
+ """Load IPAdapter presets from yaml/ipadapter.yaml for SD1.5 and SDXL."""
38
+ ipadapter_yaml_path = os.path.join(_YAML_DIR, "ipadapter.yaml")
39
+ data = _load_yaml(ipadapter_yaml_path)
40
+ res = {}
41
+ for arch in ("SD1.5", "SDXL"):
42
+ std = data.get("IPAdapter_presets", {}).get(arch, [])
43
+ face = data.get("IPAdapter_FaceID_presets", {}).get(arch, [])
44
+ res[arch] = list(std) + list(face)
45
+ return res
46
+
47
+
48
  def _parse_image_param(image_param: Any) -> Any:
49
+ """Parse a Base64 Data URI, HTTP/HTTPS URL, or PIL.Image into a PIL Image object."""
50
  if isinstance(image_param, Image.Image):
51
  return image_param
52
 
 
55
 
56
  image_param = image_param.strip()
57
 
58
+ # HTTP / HTTPS URL — download the image
59
  if image_param.startswith("http://") or image_param.startswith("https://"):
60
+ return _download_image_from_url(image_param)
 
 
61
 
62
  # Base64 Data URI (e.g. data:image/png;base64,...)
63
  if image_param.startswith("data:image/"):
 
66
  return Image.open(io.BytesIO(data))
67
 
68
  # Base64 string without header
69
+ if len(image_param) > 100:
70
  try:
71
  data = base64.b64decode(image_param)
72
  return Image.open(io.BytesIO(data))
73
  except Exception:
74
  pass
75
 
 
 
 
 
76
  raise ValueError(
77
+ "Invalid image parameter format. Expected a Base64 Data URI (e.g., 'data:image/png;base64,...') "
78
+ "or an HTTP/HTTPS URL."
79
  )
80
 
81
 
82
+ def _download_image_from_url(url: str) -> Image.Image:
83
+ """Download an image from an HTTP/HTTPS URL and return it as a PIL Image.
84
+
85
+ Security measures:
86
+ - Timeout to prevent hanging on slow/malicious servers.
87
+ - Response size cap to prevent memory exhaustion.
88
+ - Content-Type validation to reject non-image responses.
89
+ """
90
+ req = urllib.request.Request(url, headers={"User-Agent": "ImageGen-MCP/1.0"})
91
+ try:
92
+ with urllib.request.urlopen(req, timeout=_IMAGE_DOWNLOAD_TIMEOUT) as resp:
93
+ # Validate content type
94
+ content_type = resp.headers.get("Content-Type", "").split(";")[0].strip().lower()
95
+ if content_type and content_type not in _ALLOWED_IMAGE_CONTENT_TYPES:
96
+ raise ValueError(
97
+ f"URL returned non-image Content-Type '{content_type}'. "
98
+ f"Expected one of: {', '.join(sorted(_ALLOWED_IMAGE_CONTENT_TYPES))}."
99
+ )
100
+
101
+ # Enforce size limit
102
+ content_length = resp.headers.get("Content-Length")
103
+ if content_length and int(content_length) > _MAX_IMAGE_DOWNLOAD_BYTES:
104
+ raise ValueError(
105
+ f"Image at URL is too large ({int(content_length)} bytes). "
106
+ f"Maximum allowed size is {_MAX_IMAGE_DOWNLOAD_BYTES} bytes."
107
+ )
108
+
109
+ # Stream-read with size cap
110
+ chunks = []
111
+ total = 0
112
+ while True:
113
+ chunk = resp.read(8192)
114
+ if not chunk:
115
+ break
116
+ total += len(chunk)
117
+ if total > _MAX_IMAGE_DOWNLOAD_BYTES:
118
+ raise ValueError(
119
+ f"Image download exceeded maximum allowed size of "
120
+ f"{_MAX_IMAGE_DOWNLOAD_BYTES} bytes."
121
+ )
122
+ chunks.append(chunk)
123
+
124
+ data = b"".join(chunks)
125
+
126
+ except urllib.error.URLError as e:
127
+ raise ValueError(f"Failed to download image from URL: {e}") from e
128
+ except urllib.error.HTTPError as e:
129
+ raise ValueError(f"HTTP error {e.code} when downloading image from URL: {e.reason}") from e
130
+
131
+ if not data:
132
+ raise ValueError("Downloaded image data is empty.")
133
+
134
+ return Image.open(io.BytesIO(data))
135
+
136
+
137
  def _load_yaml(filepath: str) -> dict:
138
  """Safely load a YAML file, returning an empty dict if the file does not exist."""
139
  if not os.path.exists(filepath):
 
182
  "display_name": "Hi-Res Fix / Upscale",
183
  "description": "Enhance details and upscale an existing low-resolution image.",
184
  "required_inputs": ["prompt", "image", "upscale_by"],
185
+ "optional_inputs": ["upscaler", "denoise"] + _COMMON_OPTIONAL_INPUTS,
186
  },
187
  ]
188
 
 
297
  ui_inputs["feathering"] = params.get("feathering", 10)
298
  elif task_type == "hires_fix":
299
  ui_inputs["hires_image"] = pil_img
300
+ upscaler = params.get("upscaler", "nearest-exact")
301
+ if upscaler == "latent" or upscaler not in ["nearest-exact", "bilinear", "area", "bicubic", "bislerp"]:
302
+ upscaler = "nearest-exact"
303
+ ui_inputs["hires_upscaler"] = upscaler
304
  ui_inputs["hires_scale_by"] = params.get("upscale_by", 2.0)
305
  ui_inputs["hires_denoise"] = params.get("denoise", 0.55)
306
 
307
  chain = params.get("chain", [])
308
  if chain:
309
  lora_data = []
310
+ embedding_data = []
311
  controlnet_data = []
312
+ diffsynth_controlnet_data = []
313
  ipadapter_data = []
314
+ ipadapter_images = []
315
+ ipadapter_weights = []
316
+ ipadapter_lora_strengths = []
317
+ ipadapter_global_preset = params.get("ipadapter_preset") or params.get("preset")
318
+ ipadapter_global_embeds_scaling = params.get("ipadapter_embeds_scaling") or params.get("embeds_scaling")
319
+ ipadapter_global_combine_method = params.get("ipadapter_combine_method") or params.get("combine_method")
320
+ ipadapter_global_final_weight = params.get("ipadapter_final_weight") or params.get("final_weight")
321
+ flux1_ipadapter_images = []
322
+ flux1_ipadapter_weights = []
323
+ flux1_ipadapter_starts = []
324
+ flux1_ipadapter_ends = []
325
+ sd3_ipadapter_images = []
326
+ sd3_ipadapter_weights = []
327
+ sd3_ipadapter_starts = []
328
+ sd3_ipadapter_ends = []
329
+ style_images = []
330
+ style_strengths = []
331
+ krea2_identity_edit_data = []
332
+ krea2_reference_edit_data = []
333
+ krea2_controlnet_data = []
334
+ anima_controlnet_lllite_data = []
335
+ reference_latent_data = []
336
+ reference_image_data = []
337
+ joyai_reference_data = []
338
+ boogu_edit_data = []
339
+ qwen_image_edit_data = []
340
+ hidream_o1_reference_data = []
341
+ cond_prompts = []
342
+ cond_widths = []
343
+ cond_heights = []
344
+ cond_xs = []
345
+ cond_ys = []
346
+ cond_strengths = []
347
 
348
  for item in chain:
349
  itype = item.get("injector_type")
350
  if itype == "lora":
351
  lora_data.extend([
352
+ item.get("source", item.get("lora_source", "Civitai")),
353
  item.get("lora_value", ""),
354
  item.get("scale", 1.0),
355
  None
356
  ])
357
+ elif itype == "embedding":
358
+ e_source = item.get("source", item.get("embedding_source", "Civitai"))
359
+ e_val = item.get("embedding_value", item.get("value", item.get("embedding_id", "")))
360
+ if e_source and e_val:
361
+ embedding_data.extend([
362
+ e_source,
363
+ str(e_val),
364
+ None
365
+ ])
366
+ elif itype == "conditioning":
367
+ p = item.get("prompt", "")
368
+ if p:
369
+ cond_prompts.append(p)
370
+ cond_widths.append(int(item.get("width", 512)))
371
+ cond_heights.append(int(item.get("height", 512)))
372
+ cond_xs.append(int(item.get("x", 0)))
373
+ cond_ys.append(int(item.get("y", 0)))
374
+ cond_strengths.append(float(item.get("strength", 1.0)))
375
+ elif itype == "controlnet":
376
+ cn_type = item.get("type", item.get("Type", ""))
377
+ cn_series = item.get("series", item.get("Series", ""))
378
+ cn_strength = float(item.get("strength", 1.0))
379
+ cn_img = _parse_image_param(item.get("image"))
380
+
381
+ cn_filepath = item.get("control_net_name", "None")
382
+ cn_raw = _load_yaml(os.path.join(_YAML_DIR, "controlnet_models.yaml")).get("ControlNet", {})
383
+
384
+ cn_arch_key = None
385
+ if found_arch:
386
+ arch_cfg = _load_yaml(os.path.join(_YAML_DIR, "model_architectures.yaml")).get("architectures", {})
387
+ cn_arch_key = arch_cfg.get(found_arch, {}).get("controlnet_key", found_arch)
388
+
389
+ arch_entries = []
390
+ if cn_arch_key and cn_arch_key in cn_raw:
391
+ arch_entries = cn_raw[cn_arch_key]
392
+ elif found_arch and found_arch in cn_raw:
393
+ arch_entries = cn_raw[found_arch]
394
+ else:
395
+ for val in cn_raw.values():
396
+ if isinstance(val, list):
397
+ arch_entries.extend(val)
398
+ elif isinstance(val, dict):
399
+ arch_entries.append(val)
400
+
401
+ if arch_entries:
402
+ for entry in arch_entries:
403
+ entry_types = entry.get("Type", [])
404
+ if isinstance(entry_types, str):
405
+ entry_types = [entry_types]
406
+ if not cn_type or cn_type in entry_types:
407
+ if not cn_series or entry.get("Series") == cn_series:
408
+ cn_filepath = entry.get("Filepath", cn_filepath)
409
+ if not cn_series:
410
+ cn_series = entry.get("Series", "")
411
+ if not cn_type and entry_types:
412
+ cn_type = entry_types[0]
413
+ break
414
+
415
  controlnet_data.extend([
416
+ cn_img,
417
+ cn_type,
418
+ cn_series,
419
+ cn_strength,
420
+ cn_filepath
421
+ ])
422
+ elif itype == "anima_controlnet_lllite":
423
+ cn_type = item.get("type", item.get("Type", ""))
424
+ cn_series = item.get("series", item.get("Series", ""))
425
+ cn_strength = float(item.get("strength", 1.0))
426
+ cn_start = float(item.get("start_percent", 0.0))
427
+ cn_end = float(item.get("end_percent", 1.0))
428
+ cn_img = _parse_image_param(item.get("image"))
429
+
430
+ cn_filepath = item.get("control_net_name", "None")
431
+ anima_cfg = _load_yaml(os.path.join(_YAML_DIR, "anima_controlnet_lllite_models.yaml")).get("Anima_ControlNet_Lllite", [])
432
+ if anima_cfg:
433
+ for entry in anima_cfg:
434
+ entry_types = entry.get("Type", [])
435
+ if isinstance(entry_types, str):
436
+ entry_types = [entry_types]
437
+ if not cn_type or cn_type in entry_types:
438
+ if not cn_series or entry.get("Series") == cn_series:
439
+ cn_filepath = entry.get("Filepath", cn_filepath)
440
+ if not cn_series:
441
+ cn_series = entry.get("Series", "")
442
+ if not cn_type and entry_types:
443
+ cn_type = entry_types[0]
444
+ break
445
+
446
+ anima_controlnet_lllite_data.extend([
447
+ cn_img,
448
+ cn_type,
449
+ cn_series,
450
+ cn_strength,
451
+ cn_filepath,
452
+ cn_start,
453
+ cn_end
454
  ])
455
+ elif itype == "diffsynth_controlnet":
456
+ cn_type = item.get("type", "")
457
+ cn_series = item.get("series", "")
458
+ cn_strength = float(item.get("strength", 1.0))
459
+ cn_img = _parse_image_param(item.get("image"))
460
+
461
+ cn_filepath = "None"
462
+ diffsynth_raw = _load_yaml(os.path.join(_YAML_DIR, "diffsynth_controlnet_models.yaml")).get("DiffSynth_ControlNet", {})
463
+ diffsynth_entries = []
464
+ if isinstance(diffsynth_raw, dict):
465
+ for val in diffsynth_raw.values():
466
+ if isinstance(val, list):
467
+ diffsynth_entries.extend(val)
468
+ elif isinstance(val, dict):
469
+ diffsynth_entries.append(val)
470
+ elif isinstance(diffsynth_raw, list):
471
+ diffsynth_entries = diffsynth_raw
472
+
473
+ if diffsynth_entries:
474
+ for entry in diffsynth_entries:
475
+ if not cn_type or cn_type in entry.get("Type", []):
476
+ if not cn_series or entry.get("Series") == cn_series:
477
+ cn_filepath = entry.get("Filepath", cn_filepath)
478
+ if not cn_series:
479
+ cn_series = entry.get("Series", cn_series)
480
+ if not cn_type and entry.get("Type"):
481
+ cn_type = entry.get("Type")[0]
482
+ break
483
+
484
+ diffsynth_controlnet_data.extend([
485
+ cn_img,
486
+ cn_type,
487
+ cn_series,
488
+ cn_strength,
489
+ cn_filepath
490
  ])
491
+ elif itype == "krea2_controlnet":
492
+ cn_type = item.get("type", "Depth")
493
+ cn_series = item.get("series", "Patil")
494
+ cn_strength = float(item.get("strength", 1.0))
495
+ cn_img = _parse_image_param(item.get("image"))
496
+
497
+ cn_filepath = "depth-control-lora.safetensors"
498
+ krea2_cfg = _load_yaml(os.path.join(_YAML_DIR, "krea2_controlnet_models.yaml")).get("Krea2_ControlNet", [])
499
+ if krea2_cfg:
500
+ for entry in krea2_cfg:
501
+ if cn_type in entry.get("Type", []):
502
+ if not cn_series or entry.get("Series") == cn_series:
503
+ cn_filepath = entry.get("Filepath", cn_filepath)
504
+ cn_series = entry.get("Series", cn_series)
505
+ break
506
+
507
+ krea2_controlnet_data.extend([
508
+ cn_img,
509
+ cn_type,
510
+ cn_series,
511
+ cn_strength,
512
+ cn_filepath
513
  ])
514
+ elif itype == "flux1_ipadapter":
515
+ if len(flux1_ipadapter_images) < 5:
516
+ img = _parse_image_param(item.get("image"))
517
+ weight = float(item.get("weight", 1.0))
518
+ start_at = float(item.get("start_at", item.get("start_percent", item.get("start", 0.0))))
519
+ end_at = float(item.get("end_at", item.get("end_percent", item.get("end", 1.0))))
520
+ flux1_ipadapter_images.append(img)
521
+ flux1_ipadapter_weights.append(weight)
522
+ flux1_ipadapter_starts.append(start_at)
523
+ flux1_ipadapter_ends.append(end_at)
524
+ elif itype == "sd3_ipadapter":
525
+ if len(sd3_ipadapter_images) < 5:
526
+ img = _parse_image_param(item.get("image"))
527
+ weight = float(item.get("weight", 1.0))
528
+ start_at = float(item.get("start_at", item.get("start_percent", item.get("start", 0.0))))
529
+ end_at = float(item.get("end_at", item.get("end_percent", item.get("end", 1.0))))
530
+ sd3_ipadapter_images.append(img)
531
+ sd3_ipadapter_weights.append(weight)
532
+ sd3_ipadapter_starts.append(start_at)
533
+ sd3_ipadapter_ends.append(end_at)
534
+ elif itype == "ipadapter":
535
+ if len(ipadapter_images) < 5:
536
+ img = _parse_image_param(item.get("image"))
537
+ weight = float(item.get("weight", 1.0))
538
+ lora_str = float(item.get("lora_strength", 0.6))
539
+ ipadapter_images.append(img)
540
+ ipadapter_weights.append(weight)
541
+ ipadapter_lora_strengths.append(lora_str)
542
+
543
+ if "preset" in item and not ipadapter_global_preset:
544
+ ipadapter_global_preset = item["preset"]
545
+ if "embeds_scaling" in item and not ipadapter_global_embeds_scaling:
546
+ ipadapter_global_embeds_scaling = item["embeds_scaling"]
547
+ if "combine_method" in item and not ipadapter_global_combine_method:
548
+ ipadapter_global_combine_method = item["combine_method"]
549
+ if "final_weight" in item and ipadapter_global_final_weight is None:
550
+ ipadapter_global_final_weight = float(item["final_weight"])
551
+ elif itype in ("style", "flux1_style"):
552
+ img = _parse_image_param(item.get("image"))
553
+ if img:
554
+ style_images.append(img)
555
+ style_strengths.append(float(item.get("strength", item.get("weight", 1.0))))
556
+ elif itype == "pid":
557
+ is_enabled = item.get("enabled", True)
558
+ if isinstance(is_enabled, str):
559
+ is_enabled = is_enabled.upper() in ("ON", "TRUE", "1")
560
+ ui_inputs["pid_settings"] = "ON" if is_enabled else "OFF"
561
+ elif itype == "krea2_identity_edit":
562
+ img = _parse_image_param(item.get("image"))
563
+ if img:
564
+ krea2_identity_edit_data.append(img)
565
+ elif itype == "krea2_style_reference":
566
+ img = _parse_image_param(item.get("image"))
567
+ if img:
568
+ krea2_reference_edit_data.append(img)
569
+ elif itype in ("reference_latent", "reference_edit"):
570
+ img = _parse_image_param(item.get("image"))
571
+ if img:
572
+ reference_latent_data.append(img)
573
+ elif itype in ("reference_image", "mage_flow_reference_edit"):
574
+ img = _parse_image_param(item.get("image"))
575
+ if img:
576
+ reference_image_data.append(img)
577
+ elif itype in ("joyai_image", "joyai_reference_edit"):
578
+ img = _parse_image_param(item.get("image"))
579
+ if img:
580
+ joyai_reference_data.append(img)
581
+ elif itype in ("boogu_image_edit", "boogu_edit"):
582
+ img = _parse_image_param(item.get("image"))
583
+ if img:
584
+ boogu_edit_data.append(img)
585
+ elif itype == "qwen_image_edit":
586
+ img = _parse_image_param(item.get("image"))
587
+ if img:
588
+ qwen_image_edit_data.append(img)
589
+ elif itype == "hidream_o1_reference":
590
+ img = _parse_image_param(item.get("image"))
591
+ if img:
592
+ hidream_o1_reference_data.append(img)
593
+ elif itype == "vae":
594
+ v_source = item.get("source", item.get("vae_source", "Civitai"))
595
+ v_val = item.get("vae_value", item.get("value", item.get("vae_id", item.get("vae_name", ""))))
596
+ if v_source and v_val:
597
+ ui_inputs["vae_source"] = v_source
598
+ ui_inputs["vae_id"] = str(v_val)
599
 
600
  if lora_data: ui_inputs["lora_data"] = lora_data
601
+ if embedding_data: ui_inputs["embedding_data"] = embedding_data
602
  if controlnet_data: ui_inputs["controlnet_data"] = controlnet_data
603
+ if anima_controlnet_lllite_data: ui_inputs["anima_controlnet_lllite_data"] = anima_controlnet_lllite_data
604
+ if diffsynth_controlnet_data: ui_inputs["diffsynth_controlnet_data"] = diffsynth_controlnet_data
605
+ if krea2_controlnet_data: ui_inputs["krea2_controlnet_data"] = krea2_controlnet_data
606
+ if ipadapter_images:
607
+ preset = ipadapter_global_preset or "STANDARD (medium strength)"
608
+ embeds_scaling = ipadapter_global_embeds_scaling or "V only"
609
+ combine_method = ipadapter_global_combine_method or "concat"
610
+ final_weight = float(ipadapter_global_final_weight) if ipadapter_global_final_weight is not None else 1.0
611
+ final_lora_strength = 0.6
612
+
613
+ presets_by_arch = _get_ipadapter_presets_by_arch()
614
+ target_arch = "SD1.5" if found_arch in ("sd15", "SD1.5") else "SDXL"
615
+ allowed_presets = presets_by_arch.get(target_arch, [])
616
+
617
+ if preset not in allowed_presets:
618
+ raise ValueError(
619
+ f"Invalid IPAdapter preset '{preset}' for model architecture '{target_arch}'. "
620
+ f"Preset must match the target model architecture. Allowed presets for {target_arch}: {allowed_presets}"
621
+ )
622
+
623
+ ui_inputs["ipadapter_data"] = (
624
+ ipadapter_images + ipadapter_weights + ipadapter_lora_strengths +
625
+ [preset, final_weight, final_lora_strength, embeds_scaling, combine_method]
626
+ )
627
+ elif ipadapter_data:
628
+ ui_inputs["ipadapter_data"] = ipadapter_data
629
+ if flux1_ipadapter_images:
630
+ ui_inputs["flux1_ipadapter_data"] = (
631
+ flux1_ipadapter_images + flux1_ipadapter_weights + flux1_ipadapter_starts + flux1_ipadapter_ends
632
+ )
633
+ if sd3_ipadapter_images:
634
+ ui_inputs["sd3_ipadapter_chain"] = (
635
+ sd3_ipadapter_images + sd3_ipadapter_weights + sd3_ipadapter_starts + sd3_ipadapter_ends
636
+ )
637
+ if style_images: ui_inputs["style_data"] = style_images + style_strengths
638
+ if krea2_identity_edit_data: ui_inputs["krea2_identity_edit_data"] = krea2_identity_edit_data
639
+ if krea2_reference_edit_data: ui_inputs["krea2_reference_edit_data"] = krea2_reference_edit_data
640
+ if reference_latent_data: ui_inputs["reference_latent_data"] = reference_latent_data
641
+ if reference_image_data: ui_inputs["reference_image_data"] = reference_image_data
642
+ if joyai_reference_data: ui_inputs["joyai_reference_data"] = joyai_reference_data
643
+ if boogu_edit_data: ui_inputs["boogu_edit_data"] = boogu_edit_data
644
+ if qwen_image_edit_data: ui_inputs["qwen_image_edit_data"] = qwen_image_edit_data
645
+ if hidream_o1_reference_data: ui_inputs["hidream_o1_reference_data"] = hidream_o1_reference_data
646
+ if cond_prompts:
647
+ ui_inputs["conditioning_data"] = (
648
+ cond_prompts + cond_widths + cond_heights + cond_xs + cond_ys + cond_strengths
649
+ )
650
+
651
+ if "vae_source" in params and "vae_id" in params:
652
+ ui_inputs["vae_source"] = params["vae_source"]
653
+ ui_inputs["vae_id"] = str(params["vae_id"])
654
+
655
+ pid_val = params.get("pid") if params.get("pid") is not None else params.get("pid_settings")
656
+ if pid_val is not None:
657
+ if isinstance(pid_val, bool):
658
+ ui_inputs["pid_settings"] = "ON" if pid_val else "OFF"
659
+ elif str(pid_val).upper() in ("ON", "TRUE", "1"):
660
+ ui_inputs["pid_settings"] = "ON"
661
+ else:
662
+ ui_inputs["pid_settings"] = "OFF"
663
 
664
  _TASKS_DB[task_id]["progress"] = 50
665
 
mcp_tools/get_chain_schema.py DELETED
@@ -1,32 +0,0 @@
1
- """
2
- MCP Tool: get_chain_schema
3
- Get the complete parameter schema and usage guide for a specified chain/injector type.
4
- """
5
-
6
- from .common import _load_yaml, _CHAIN_FEATURES_PATH
7
- from .error_schema import make_validation_error, make_not_found_error
8
-
9
-
10
- def handle_get_chain_schema(chain_type: str) -> dict:
11
- """Get the complete parameter schema and usage guide for a specified chain/injector type."""
12
- if not chain_type:
13
- return make_validation_error(
14
- "Parameter 'chain_type' is required.",
15
- missing_fields=["chain_type"],
16
- )
17
-
18
- chain_features = _load_yaml(_CHAIN_FEATURES_PATH)
19
-
20
- if chain_type not in chain_features:
21
- return make_not_found_error("chain_type", chain_type)
22
-
23
- chain_data = chain_features[chain_type]
24
- return {
25
- "feature_name": chain_type,
26
- "display_name": chain_data.get("display_name", chain_type),
27
- "description": chain_data.get("description", ""),
28
- "supported_tasks": chain_data.get("supported_tasks", []),
29
- "max_count": chain_data.get("max_count", 1),
30
- "usage_guideline": chain_data.get("usage_guideline", ""),
31
- "parameters_schema": chain_data.get("parameters_schema", {}),
32
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
mcp_tools/get_feature_list.py CHANGED
@@ -1,29 +1,127 @@
1
- """
2
- MCP Tool: get_feature_list
3
- Get the list of supported advanced features along with their usage constraints and parameter schemas.
4
- """
5
 
6
- from .common import _load_yaml, _CHAIN_FEATURES_PATH
7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
 
9
- def handle_get_feature_list() -> list:
10
- """Dynamically load the list of supported advanced features from chain_features.yaml."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  chain_features = _load_yaml(_CHAIN_FEATURES_PATH)
12
- result = []
13
-
14
- for chain_name, chain_data in chain_features.items():
15
- if chain_data.get("visibility", "public") != "public":
16
- continue
17
-
18
- entry = {
19
- "feature_name": chain_name,
20
- "display_name": chain_data.get("display_name", chain_name),
21
- "description": chain_data.get("description", ""),
22
- "supported_tasks": chain_data.get("supported_tasks", []),
23
- "max_count": chain_data.get("max_count", 1),
24
- "usage_guideline": chain_data.get("usage_guideline", ""),
25
- "parameters_schema": chain_data.get("parameters_schema", {}),
26
- }
27
- result.append(entry)
28
-
29
- return result
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from copy import deepcopy
3
+ from .common import _load_yaml, _CHAIN_FEATURES_PATH, _YAML_DIR
4
+ from .error_schema import make_not_found_error
5
 
 
6
 
7
+ def _build_feature_entry(chain_name: str, chain_data: dict, include_schema: bool = False) -> dict:
8
+ entry = {
9
+ "feature_name": chain_name,
10
+ "chains": chain_data.get("chains", chain_name),
11
+ "display_name": chain_data.get("display_name", chain_name),
12
+ "description": chain_data.get("description", ""),
13
+ "supported_tasks": chain_data.get("supported_tasks", []),
14
+ "max_count": chain_data.get("max_count", 1),
15
+ "usage_guideline": chain_data.get("usage_guideline", ""),
16
+ }
17
+ if include_schema:
18
+ schema = deepcopy(chain_data.get("parameters_schema", {}))
19
+ if chain_name in ("krea2_controlnet", "diffsynth_controlnet", "controlnet", "anima_controlnet_lllite"):
20
+ config_key = (
21
+ "Krea2_ControlNet" if chain_name == "krea2_controlnet"
22
+ else "DiffSynth_ControlNet" if chain_name == "diffsynth_controlnet"
23
+ else "Anima_ControlNet_Lllite" if chain_name == "anima_controlnet_lllite"
24
+ else "ControlNet"
25
+ )
26
+ yaml_filename = f"{chain_name}_models.yaml"
27
+ model_path = os.path.join(_YAML_DIR, yaml_filename)
28
+ raw_models = _load_yaml(model_path).get(config_key, [])
29
+ models_list = []
30
+ if isinstance(raw_models, dict):
31
+ for val in raw_models.values():
32
+ if isinstance(val, list):
33
+ models_list.extend(val)
34
+ elif isinstance(val, dict):
35
+ models_list.append(val)
36
+ elif isinstance(raw_models, list):
37
+ models_list = raw_models
38
 
39
+ types_set = set()
40
+ for m in models_list:
41
+ t_val = m.get("Type", [])
42
+ if isinstance(t_val, list):
43
+ types_set.update(t_val)
44
+ elif isinstance(t_val, str):
45
+ types_set.add(t_val)
46
+ types = sorted(list(types_set))
47
+ series = sorted(list(set(m.get("Series") for m in models_list if m.get("Series"))))
48
+ if "properties" in schema:
49
+ if "type" in schema["properties"] and types:
50
+ schema["properties"]["type"]["enum"] = types
51
+ if "series" in schema["properties"] and series:
52
+ schema["properties"]["series"]["enum"] = series
53
+ if chain_name == "controlnet" and isinstance(raw_models, dict):
54
+ schema["architectures"] = raw_models
55
+ elif chain_name == "ipadapter":
56
+ from .common import _get_ipadapter_presets_by_arch
57
+ presets_by_arch = _get_ipadapter_presets_by_arch()
58
+ schema["presets_by_architecture"] = presets_by_arch
59
+ all_presets = sorted(list(set(presets_by_arch.get("SD1.5", []) + presets_by_arch.get("SDXL", []))))
60
+ if "properties" in schema and "preset" in schema["properties"]:
61
+ schema["properties"]["preset"]["enum"] = all_presets
62
+ entry["parameters_schema"] = schema
63
+ return entry
64
+
65
+
66
+ def handle_get_feature_list(feature_name: str | list[str] = "") -> list | dict:
67
+ """
68
+ Dynamically load supported advanced features from chain_features.yaml.
69
+
70
+ - If feature_name is empty: returns a summary list of ALL features (excluding parameters_schema)
71
+ to optimize response size and token usage.
72
+ - If feature_name is specified (single feature name, comma-separated string, or list of strings):
73
+ returns complete feature details INCLUDING parameters_schema for the requested feature(s).
74
+ """
75
  chain_features = _load_yaml(_CHAIN_FEATURES_PATH)
76
+
77
+ targets = []
78
+ is_single_string_query = False
79
+
80
+ if isinstance(feature_name, list):
81
+ targets = [str(x).strip() for x in feature_name if str(x).strip()]
82
+ elif isinstance(feature_name, str) and feature_name.strip():
83
+ raw_str = feature_name.strip()
84
+ parts = [x.strip() for x in raw_str.split(",") if x.strip()]
85
+ targets = parts
86
+ if len(parts) == 1 and "," not in raw_str:
87
+ is_single_string_query = True
88
+
89
+ # Case 1: Empty input -> return summary list of all features (without parameters_schema)
90
+ if not targets:
91
+ return [
92
+ _build_feature_entry(name, data, include_schema=False)
93
+ for name, data in chain_features.items()
94
+ ]
95
+
96
+ # Helper function to resolve feature target by key or chains alias
97
+ def _resolve_target(target_name: str) -> str | None:
98
+ if target_name in chain_features:
99
+ return target_name
100
+ for feat_key, feat_data in chain_features.items():
101
+ feat_chains = feat_data.get("chains")
102
+ if isinstance(feat_chains, str) and feat_chains == target_name:
103
+ return feat_key
104
+ elif isinstance(feat_chains, list) and target_name in feat_chains:
105
+ return feat_key
106
+ return None
107
+
108
+ resolved_targets = []
109
+ # Case 2: Specific feature(s) requested -> validate existence
110
+ for target in targets:
111
+ resolved = _resolve_target(target)
112
+ if not resolved:
113
+ return make_not_found_error("feature_name", target)
114
+ resolved_targets.append(resolved)
115
+
116
+ # Case 3: Return full info including parameters_schema
117
+ results = [
118
+ _build_feature_entry(target, chain_features[target], include_schema=True)
119
+ for target in resolved_targets
120
+ ]
121
+
122
+ if is_single_string_query and len(results) == 1:
123
+ return results[0]
124
+
125
+ return results
126
+
127
+
mcp_tools/get_model_features.py CHANGED
@@ -52,24 +52,15 @@ def handle_get_model_features(model: str) -> dict:
52
  enabled_chains = arch_features.get("enabled_chains", [])
53
 
54
  supported_features = []
55
- for chain_name in enabled_chains:
56
- if chain_name in chain_features:
57
- chain_data = chain_features[chain_name]
58
- visibility = chain_data.get("visibility", "public")
59
- if visibility == "public":
60
- supported_features.append(chain_name)
61
- else:
62
- generic_mapping = {
63
- "krea2_controlnet": "controlnet",
64
- "anima_controlnet_lllite": "controlnet",
65
- "controlnet_model_patch": "controlnet",
66
- "flux1_ipadapter": "ipadapter",
67
- "sd3_ipadapter": "ipadapter",
68
- "hidream_o1_reference": "reference_latent",
69
- }
70
- generic_name = generic_mapping.get(chain_name)
71
- if generic_name and generic_name not in supported_features:
72
- supported_features.append(generic_name)
73
 
74
  arch_defaults_section = model_defaults.get(found_arch, {})
75
  arch_level_defaults = arch_defaults_section.get("_defaults", {})
 
52
  enabled_chains = arch_features.get("enabled_chains", [])
53
 
54
  supported_features = []
55
+ for feat_name, feat_data in chain_features.items():
56
+ feat_chains = feat_data.get("chains")
57
+ if feat_chains is None:
58
+ feat_chains = [feat_name]
59
+ elif isinstance(feat_chains, str):
60
+ feat_chains = [feat_chains]
61
+
62
+ if any(c in enabled_chains for c in feat_chains):
63
+ supported_features.append(feat_name)
 
 
 
 
 
 
 
 
 
64
 
65
  arch_defaults_section = model_defaults.get(found_arch, {})
66
  arch_level_defaults = arch_defaults_section.get("_defaults", {})
mcp_tools/mcp_gradio_integration.py CHANGED
@@ -2,7 +2,7 @@
2
  MCP & Gradio Integration Module
3
 
4
  Provides:
5
- 1. register_high_level_mcp_apis: Expose only 8 high-level abstract API/MCP endpoints (using gr.api without polluting the visual UI structure)
6
  2. cleanup_dependencies_api_names: Force cleanup of show_api attribute for non-high-level APIs in dependencies
7
  3. patch_gradio_api_suppression: No-op implementation retained for backward compatibility
8
  """
@@ -15,8 +15,7 @@ from .get_model_architecture_list import handle_get_model_architecture_list
15
  from .get_model_list import handle_get_model_list
16
  from .get_feature_list import handle_get_feature_list
17
  from .get_model_features import handle_get_model_features
18
- from .get_chain_schema import handle_get_chain_schema
19
- from .run_imagegen import handle_run_imagegen
20
  from .get_task_status import handle_get_task_status
21
 
22
  HIGH_LEVEL_MCP_API_NAMES = {
@@ -25,9 +24,8 @@ HIGH_LEVEL_MCP_API_NAMES = {
25
  "get_model_list",
26
  "get_feature_list",
27
  "get_model_features",
28
- "run_imagegen",
29
  "get_task_status",
30
- "get_chain_schema",
31
  }
32
 
33
 
@@ -50,7 +48,7 @@ def patch_gradio_api_suppression():
50
  def cleanup_dependencies_api_names(demo):
51
  """
52
  Clean up residual auto-generated API names in demo.fns and demo.dependencies.
53
- Force only the 8 high-level abstract MCP APIs to be exposed as public endpoints.
54
  """
55
  for fn in demo.fns.values():
56
  api_name = getattr(fn, "api_name", None)
@@ -73,11 +71,11 @@ def cleanup_dependencies_api_names(demo):
73
 
74
  def register_high_level_mcp_apis(demo):
75
  """
76
- Explicitly register 8 high-level abstract MCP API endpoints on the Gradio demo using gr.api.
77
  Using gr.api() never adds any visual UI components (such as Row, Textbox, Button, etc.), avoiding duplicate interface rendering.
78
  """
79
  def get_task_list() -> list:
80
- """[Recommended Discovery Flow Step 1] Get a list of all supported image generation task types (txt2img, img2img, inpaint, outpaint, hires_fix) along with their required and optional parameter lists. Recommended flow: get_task_list -> get_model_architecture_list -> get_model_list -> [Path 1: Call run_imagegen directly (pass only required params) | Path 2: Call get_model_features to get official default hyperparams -> run_imagegen]."""
81
  return sanitize_keys(handle_get_task_list())
82
 
83
  def get_model_architecture_list() -> list:
@@ -85,19 +83,19 @@ def register_high_level_mcp_apis(demo):
85
  return sanitize_keys(handle_get_model_architecture_list())
86
 
87
  def get_model_list(model_architecture: str = "") -> list | dict:
88
- """[Recommended Discovery Flow Step 3] Query the list of available image generation models. After obtaining models, choose one of two paths: 1. [Path 1 (Recommended - Minimal Mode)] Call run_imagegen directly with only required parameters. Do NOT guess steps/cfg/sampler/scheduler from experience; the server will automatically apply the model's optimal default hyperparameters. 2. [Path 2 (Explicit Alignment Mode)] First call get_model_features to query the model's officially recommended hyperparameters, then pass them to run_imagegen."""
89
  arch = model_architecture.strip() if model_architecture else None
90
  return sanitize_keys(handle_get_model_list(arch))
91
 
92
- def get_feature_list() -> list:
93
- """Get the list of supported advanced features along with their usage constraints and parameter schemas."""
94
- return sanitize_keys(handle_get_feature_list())
95
 
96
  def get_model_features(model: str = "") -> dict:
97
  """Query metadata for the specified model, including supported task types, extended features, and official default inference parameters (steps, cfg, sampler, scheduler). This tool MUST be called when explicitly obtaining a model's optimal default hyperparameters (Path 2). Guessing or fabricating hyperparameters without querying is strictly prohibited."""
98
  return sanitize_keys(handle_get_model_features(model.strip()))
99
 
100
- def run_imagegen(json_params: str = "{}") -> dict:
101
  """[Recommended Discovery Flow Step 4] Unified image generation task execution interface. Supports txt2img, img2img, and other tasks with chainable extended features. [IMPORTANT PARAMETER RULES] Do NOT guess or fabricate inference hyperparameters such as steps, cfg, sampler, scheduler! Path 1 (Recommended): Pass only required parameters (task_type, model, prompt, width, height), leave optional hyperparams empty (server uses optimal defaults). Path 2: If explicit hyperparams are needed, you MUST first call get_model_features to obtain official defaults before passing them."""
102
  try:
103
  if isinstance(json_params, dict):
@@ -106,25 +104,20 @@ def register_high_level_mcp_apis(demo):
106
  params = json.loads(json_params or "{}")
107
  except Exception as e:
108
  return {"error": {"code": "INVALID_JSON", "message": f"Failed to parse JSON params: {e}"}}
109
- return sanitize_keys(handle_run_imagegen(params))
110
 
111
  def get_task_status(task_id: str = "") -> dict:
112
  """Query the progress, status, and final generated results of an async image generation task."""
113
  return sanitize_keys(handle_get_task_status(task_id.strip()))
114
 
115
- def get_chain_schema(chain_type: str = "") -> dict:
116
- """Get the complete parameter schema and usage examples for a specified chain/injector type."""
117
- return sanitize_keys(handle_get_chain_schema(chain_type.strip()))
118
-
119
  funcs = [
120
  get_task_list,
121
  get_model_architecture_list,
122
  get_model_list,
123
  get_feature_list,
124
  get_model_features,
125
- run_imagegen,
126
  get_task_status,
127
- get_chain_schema,
128
  ]
129
 
130
  for func in funcs:
@@ -134,4 +127,4 @@ def register_high_level_mcp_apis(demo):
134
  if getattr(fn, "api_name", None) in HIGH_LEVEL_MCP_API_NAMES:
135
  fn.show_api = True
136
 
137
- print("[MCP Integration] Successfully registered 8 High-Level Abstract MCP APIs via gr.api().")
 
2
  MCP & Gradio Integration Module
3
 
4
  Provides:
5
+ 1. register_high_level_mcp_apis: Expose only 7 high-level abstract API/MCP endpoints (using gr.api without polluting the visual UI structure)
6
  2. cleanup_dependencies_api_names: Force cleanup of show_api attribute for non-high-level APIs in dependencies
7
  3. patch_gradio_api_suppression: No-op implementation retained for backward compatibility
8
  """
 
15
  from .get_model_list import handle_get_model_list
16
  from .get_feature_list import handle_get_feature_list
17
  from .get_model_features import handle_get_model_features
18
+ from .run import handle_run
 
19
  from .get_task_status import handle_get_task_status
20
 
21
  HIGH_LEVEL_MCP_API_NAMES = {
 
24
  "get_model_list",
25
  "get_feature_list",
26
  "get_model_features",
27
+ "run",
28
  "get_task_status",
 
29
  }
30
 
31
 
 
48
  def cleanup_dependencies_api_names(demo):
49
  """
50
  Clean up residual auto-generated API names in demo.fns and demo.dependencies.
51
+ Force only the 7 high-level abstract MCP APIs to be exposed as public endpoints.
52
  """
53
  for fn in demo.fns.values():
54
  api_name = getattr(fn, "api_name", None)
 
71
 
72
  def register_high_level_mcp_apis(demo):
73
  """
74
+ Explicitly register 7 high-level abstract MCP API endpoints on the Gradio demo using gr.api.
75
  Using gr.api() never adds any visual UI components (such as Row, Textbox, Button, etc.), avoiding duplicate interface rendering.
76
  """
77
  def get_task_list() -> list:
78
+ """[Recommended Discovery Flow Step 1] Get a list of all supported image generation task types (txt2img, img2img, inpaint, outpaint, hires_fix) along with their required and optional parameter lists. Recommended flow: get_task_list -> get_model_architecture_list -> get_model_list -> [Path 1: Call run directly (pass only required params) | Path 2: Call get_model_features to get official default hyperparams -> run]."""
79
  return sanitize_keys(handle_get_task_list())
80
 
81
  def get_model_architecture_list() -> list:
 
83
  return sanitize_keys(handle_get_model_architecture_list())
84
 
85
  def get_model_list(model_architecture: str = "") -> list | dict:
86
+ """[Recommended Discovery Flow Step 3] Query the list of available image generation models. After obtaining models, choose one of two paths: 1. [Path 1 (Recommended - Minimal Mode)] Call run directly with only required parameters. Do NOT guess steps/cfg/sampler/scheduler from experience; the server will automatically apply the model's optimal default hyperparameters. 2. [Path 2 (Explicit Alignment Mode)] First call get_model_features to query the model's officially recommended hyperparameters, then pass them to run."""
87
  arch = model_architecture.strip() if model_architecture else None
88
  return sanitize_keys(handle_get_model_list(arch))
89
 
90
+ def get_feature_list(feature_name: str = "") -> list | dict:
91
+ """Get supported advanced features. If feature_name is empty, returns a summary list of ALL features (excluding parameters_schema to save tokens). Pass a specific feature_name (single name like 'lora', or comma-separated like 'lora, ipadapter') to retrieve complete details INCLUDING parameters_schema for requested feature(s)."""
92
+ return sanitize_keys(handle_get_feature_list(feature_name.strip() if isinstance(feature_name, str) else feature_name))
93
 
94
  def get_model_features(model: str = "") -> dict:
95
  """Query metadata for the specified model, including supported task types, extended features, and official default inference parameters (steps, cfg, sampler, scheduler). This tool MUST be called when explicitly obtaining a model's optimal default hyperparameters (Path 2). Guessing or fabricating hyperparameters without querying is strictly prohibited."""
96
  return sanitize_keys(handle_get_model_features(model.strip()))
97
 
98
+ def run(json_params: str = "{}") -> dict:
99
  """[Recommended Discovery Flow Step 4] Unified image generation task execution interface. Supports txt2img, img2img, and other tasks with chainable extended features. [IMPORTANT PARAMETER RULES] Do NOT guess or fabricate inference hyperparameters such as steps, cfg, sampler, scheduler! Path 1 (Recommended): Pass only required parameters (task_type, model, prompt, width, height), leave optional hyperparams empty (server uses optimal defaults). Path 2: If explicit hyperparams are needed, you MUST first call get_model_features to obtain official defaults before passing them."""
100
  try:
101
  if isinstance(json_params, dict):
 
104
  params = json.loads(json_params or "{}")
105
  except Exception as e:
106
  return {"error": {"code": "INVALID_JSON", "message": f"Failed to parse JSON params: {e}"}}
107
+ return sanitize_keys(handle_run(params))
108
 
109
  def get_task_status(task_id: str = "") -> dict:
110
  """Query the progress, status, and final generated results of an async image generation task."""
111
  return sanitize_keys(handle_get_task_status(task_id.strip()))
112
 
 
 
 
 
113
  funcs = [
114
  get_task_list,
115
  get_model_architecture_list,
116
  get_model_list,
117
  get_feature_list,
118
  get_model_features,
119
+ run,
120
  get_task_status,
 
121
  ]
122
 
123
  for func in funcs:
 
127
  if getattr(fn, "api_name", None) in HIGH_LEVEL_MCP_API_NAMES:
128
  fn.show_api = True
129
 
130
+ print("[MCP Integration] Successfully registered 7 High-Level Abstract MCP APIs via gr.api().")
mcp_tools/{run_imagegen.py → run.py} RENAMED
@@ -1,5 +1,5 @@
1
  """
2
- MCP Tool: run_imagegen
3
  Unified image generation task submission and execution interface.
4
  """
5
 
@@ -16,7 +16,7 @@ from .common import (
16
  from .error_schema import make_validation_error, make_not_found_error
17
 
18
 
19
- def handle_run_imagegen(params: dict) -> dict:
20
  """Unified image generation task execution interface."""
21
  if not isinstance(params, dict):
22
  return make_validation_error("Request params must be an object.")
 
1
  """
2
+ MCP Tool: run
3
  Unified image generation task submission and execution interface.
4
  """
5
 
 
16
  from .error_schema import make_validation_error, make_not_found_error
17
 
18
 
19
+ def handle_run(params: dict) -> dict:
20
  """Unified image generation task execution interface."""
21
  if not isinstance(params, dict):
22
  return make_validation_error("Request params must be an object.")
mcp_tools/tool_handlers.py CHANGED
@@ -1,6 +1,6 @@
1
  """
2
  MCP Tool Handlers — Backward-compatible aggregation entry point.
3
- Core logic has been split into individual files (get_*.py and run_imagegen.py).
4
  """
5
 
6
  from .get_task_list import handle_get_task_list
@@ -8,8 +8,7 @@ from .get_model_architecture_list import handle_get_model_architecture_list
8
  from .get_model_list import handle_get_model_list
9
  from .get_feature_list import handle_get_feature_list
10
  from .get_model_features import handle_get_model_features
11
- from .get_chain_schema import handle_get_chain_schema
12
- from .run_imagegen import handle_run_imagegen
13
  from .get_task_status import handle_get_task_status
14
  from .common import (
15
  _TASK_DEFINITIONS,
@@ -24,7 +23,6 @@ __all__ = [
24
  "handle_get_model_list",
25
  "handle_get_feature_list",
26
  "handle_get_model_features",
27
- "handle_get_chain_schema",
28
- "handle_run_imagegen",
29
  "handle_get_task_status",
30
  ]
 
1
  """
2
  MCP Tool Handlers — Backward-compatible aggregation entry point.
3
+ Core logic has been split into individual files (get_*.py and run.py).
4
  """
5
 
6
  from .get_task_list import handle_get_task_list
 
8
  from .get_model_list import handle_get_model_list
9
  from .get_feature_list import handle_get_feature_list
10
  from .get_model_features import handle_get_model_features
11
+ from .run import handle_run
 
12
  from .get_task_status import handle_get_task_status
13
  from .common import (
14
  _TASK_DEFINITIONS,
 
23
  "handle_get_model_list",
24
  "handle_get_feature_list",
25
  "handle_get_model_features",
26
+ "handle_run",
 
27
  "handle_get_task_status",
28
  ]
requirements.txt CHANGED
@@ -1,5 +1,5 @@
1
- comfyui-frontend-package==1.47.12
2
- comfyui-workflow-templates==0.11.27
3
  comfyui-embedded-docs==0.5.9
4
  torch
5
  torchsde
@@ -23,7 +23,7 @@ SQLAlchemy>=2.0.0
23
  filelock
24
  av>=16.0.0
25
  comfy-kitchen==0.2.26
26
- comfy-aimdo==0.4.11
27
  requests
28
  simpleeval>=1.0.0
29
  blake3
 
1
+ comfyui-frontend-package==1.48.6
2
+ comfyui-workflow-templates==0.11.31
3
  comfyui-embedded-docs==0.5.9
4
  torch
5
  torchsde
 
23
  filelock
24
  av>=16.0.0
25
  comfy-kitchen==0.2.26
26
+ comfy-aimdo==0.4.13
27
  requests
28
  simpleeval>=1.0.0
29
  blake3
yaml/chain_features.yaml ADDED
@@ -0,0 +1,652 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Complete Feature & Chain Definitions Configuration for MCP Tools
2
+ # Every chain injector in chain_injectors/ corresponds 1-to-1 with an entry here (21 injectors total).
3
+
4
+ lora:
5
+ chains: lora
6
+ display_name: "LoRA Fine-tuning Injector"
7
+ description: "Injects LoRA weights into UNet/DiT model and CLIP text encoder for custom style, character, or domain adaptation."
8
+ supported_tasks:
9
+ - txt2img
10
+ - img2img
11
+ - inpaint
12
+ - outpaint
13
+ - hires_fix
14
+ max_count: 5
15
+ usage_guideline: "Specify source ('Civitai' or 'Hugging Face'), then provide the lora_value (Civitai Version ID or HF repo path), and a single scale value (0.0~2.0) that controls both model and clip strength simultaneously."
16
+ parameters_schema:
17
+ type: object
18
+ properties:
19
+ source:
20
+ type: string
21
+ enum: ["Civitai", "Hugging Face"]
22
+ description: "Download source for the LoRA model. Use 'Civitai' to download by Version ID, or 'Hugging Face' to download by repo path."
23
+ lora_value:
24
+ type: string
25
+ description: "For Civitai: the Version ID (e.g., '456' from civitai.com/models/123?modelVersionId=456). For Hugging Face: repo_id/filename.extension or repo_id/folder_path/filename.extension (e.g., 'lightx2v/Qwen-Image-Lightning/Qwen-Image-Lightning-4steps-V2.0-bf16.safetensors')."
26
+ scale:
27
+ type: number
28
+ default: 1.0
29
+ minimum: 0.0
30
+ maximum: 2.0
31
+ description: "Unified strength applied to both the UNet/DiT model and CLIP text encoder (0.0 to 2.0)."
32
+ required:
33
+ - source
34
+ - lora_value
35
+
36
+ ipadapter:
37
+ chains: ipadapter
38
+ display_name: "IP-Adapter Image Prompt"
39
+ description: "Uses reference images to guide generation style, composition, structure, or face appearance without prompt text restrictions (SD1.5 & SDXL)."
40
+ supported_tasks:
41
+ - txt2img
42
+ - img2img
43
+ - inpaint
44
+ - outpaint
45
+ - hires_fix
46
+ max_count: 5
47
+ usage_guideline: "Supply global settings (preset, embeds_scaling, combine_method, final_weight) and up to 5 reference images with individual weights. Preset must match the target model architecture (SD1.5 or SDXL)."
48
+ parameters_schema:
49
+ type: object
50
+ properties:
51
+ image:
52
+ type: string
53
+ description: "Reference image encoded as Base64 Data URI (e.g., data:image/png;base64,...) or HTTP/HTTPS URL."
54
+ weight:
55
+ type: number
56
+ default: 1.0
57
+ minimum: 0.0
58
+ maximum: 2.0
59
+ description: "Influence weight of the individual image prompt (0.0 to 2.0)."
60
+ preset:
61
+ type: string
62
+ default: "STANDARD (medium strength)"
63
+ description: "IPAdapter preset model variant loaded from ipadapter.yaml. Must match model architecture (SD1.5 vs SDXL)."
64
+ embeds_scaling:
65
+ type: string
66
+ default: "V only"
67
+ enum:
68
+ - "V only"
69
+ - "K+V"
70
+ - "K+V w/ C penalty"
71
+ - "K+mean(V) w/ C penalty"
72
+ description: "Embedding scaling method for IPAdapter."
73
+ combine_method:
74
+ type: string
75
+ default: "concat"
76
+ enum:
77
+ - "concat"
78
+ - "add"
79
+ - "subtract"
80
+ - "average"
81
+ - "norm average"
82
+ - "max"
83
+ - "min"
84
+ description: "Combination method for multiple reference images."
85
+ final_weight:
86
+ type: number
87
+ default: 1.0
88
+ minimum: 0.0
89
+ maximum: 2.0
90
+ description: "Global weight multiplier for IPAdapter conditioning (0.0 to 2.0)."
91
+ lora_strength:
92
+ type: number
93
+ default: 0.6
94
+ description: "LoRA weight strength for FaceID adapter variants."
95
+ required:
96
+ - image
97
+
98
+ controlnet:
99
+ chains: controlnet
100
+ display_name: "ControlNet Spatial Guidance"
101
+ description: "Applies structural and spatial conditioning (depth, pose, lineart, tile, scribble, canny) to guide output composition."
102
+ supported_tasks:
103
+ - txt2img
104
+ - img2img
105
+ - inpaint
106
+ - outpaint
107
+ - hires_fix
108
+ max_count: 5
109
+ usage_guideline: "Specify ControlNet type and series (must match requested model architecture e.g., SD1.5, SDXL, SD3.5, FLUX.1, Qwen-Image), pre-processed control image (Base64 Data URI or HTTP/HTTPS URL; system does NOT pre-process raw RGB images), and guidance strength."
110
+ parameters_schema:
111
+ type: object
112
+ properties:
113
+ type:
114
+ type: string
115
+ description: "ControlNet conditioning type (must match model architecture)."
116
+ series:
117
+ type: string
118
+ description: "ControlNet model series (must match model architecture)."
119
+ image:
120
+ type: string
121
+ description: "Pre-processed control image (e.g., Depth, Canny, Pose, Lineart map) encoded as Base64 Data URI or HTTP/HTTPS URL. Note: System does NOT automatically pre-process raw RGB images."
122
+ strength:
123
+ type: number
124
+ default: 1.0
125
+ minimum: 0.0
126
+ maximum: 2.0
127
+ description: "Control influence strength (0.0 to 2.0)."
128
+ required:
129
+ - type
130
+ - series
131
+ - image
132
+
133
+ conditioning:
134
+ chains: conditioning
135
+ display_name: "Regional Conditioning / Area Prompt"
136
+ description: "Defines rectangular areas (X, Y, Width, Height) and assigns specific text prompts and conditioning strengths to them."
137
+ supported_tasks:
138
+ - txt2img
139
+ - img2img
140
+ - inpaint
141
+ - outpaint
142
+ - hires_fix
143
+ max_count: 10
144
+ usage_guideline: "Define rectangular spatial areas (X, Y, width, height) and assign specific prompts and strengths to them. Supports up to 10 area prompts."
145
+ parameters_schema:
146
+ type: object
147
+ properties:
148
+ prompt:
149
+ type: string
150
+ description: "Text prompt for this specific rectangular area."
151
+ x:
152
+ type: integer
153
+ default: 0
154
+ description: "Top-left X coordinate of the rectangular area."
155
+ y:
156
+ type: integer
157
+ default: 0
158
+ description: "Top-left Y coordinate of the rectangular area."
159
+ width:
160
+ type: integer
161
+ default: 512
162
+ description: "Width of the rectangular area."
163
+ height:
164
+ type: integer
165
+ default: 512
166
+ description: "Height of the rectangular area."
167
+ strength:
168
+ type: number
169
+ default: 1.0
170
+ minimum: 0.1
171
+ maximum: 2.0
172
+ description: "Conditioning strength for this area (0.1 to 2.0)."
173
+ required:
174
+ - prompt
175
+
176
+ vae:
177
+ chains: vae
178
+ display_name: "Custom VAE Loader"
179
+ description: "Overrides default VAE model used for latent space encoding and final image decoding."
180
+ supported_tasks:
181
+ - txt2img
182
+ - img2img
183
+ - inpaint
184
+ - outpaint
185
+ - hires_fix
186
+ max_count: 1
187
+ usage_guideline: "Specify source ('Civitai' or 'Hugging Face'), then provide the vae_value (Civitai Version ID or HF file path)."
188
+ parameters_schema:
189
+ type: object
190
+ properties:
191
+ source:
192
+ type: string
193
+ enum: ["Civitai", "Hugging Face"]
194
+ description: "Download source for the VAE model. Use 'Civitai' to download by Version ID, or 'Hugging Face' to download by repo path."
195
+ vae_value:
196
+ type: string
197
+ description: "For Civitai: the Version ID (e.g., '456' from civitai.com/models/123?modelVersionId=456). For Hugging Face: repo_id/filename.extension or repo_id/folder_path/filename.extension (e.g., 'madebyollin/sdxl-vae-fp16-fix/sdxl_vae.safetensors')."
198
+ required:
199
+ - source
200
+ - vae_value
201
+
202
+ pid:
203
+ chains: pid
204
+ display_name: "PiD High-Resolution Refinement"
205
+ description: "Progressive Detail (PiD) upscale injector for fine detail enhancement and resolution upscaling."
206
+ supported_tasks:
207
+ - txt2img
208
+ max_count: 1
209
+ usage_guideline: "Enables PiD detail refinement pipeline using a boolean switch ('enabled': true/false)."
210
+ parameters_schema:
211
+ type: object
212
+ properties:
213
+ enabled:
214
+ type: boolean
215
+ default: true
216
+ description: "Enable or disable PiD High-Resolution Refinement."
217
+ required:
218
+ - enabled
219
+
220
+ flux1_style:
221
+ chains: style
222
+ display_name: "FLUX.1 Style Reference"
223
+ description: "Applies artistic style conditioning from reference images onto FLUX.1 model generated outputs."
224
+ supported_tasks:
225
+ - txt2img
226
+ - img2img
227
+ - inpaint
228
+ - outpaint
229
+ - hires_fix
230
+ max_count: 5
231
+ usage_guideline: "Supply style reference image(s) (up to 5) encoded as Base64 Data URI or HTTP/HTTPS URL and optional strength."
232
+ parameters_schema:
233
+ type: object
234
+ properties:
235
+ image:
236
+ type: string
237
+ description: "Style reference image encoded as Base64 Data URI or HTTP/HTTPS URL."
238
+ strength:
239
+ type: number
240
+ default: 1.0
241
+ minimum: 0.0
242
+ maximum: 2.0
243
+ description: "Style influence strength (0.0 to 2.0)."
244
+ required:
245
+ - image
246
+
247
+ reference_edit:
248
+ chains: reference_latent
249
+ display_name: "Reference Edit"
250
+ description: "For multimodal models, this feature enables powerful editing and combining capabilities. In txt2img mode, adding a single reference image performs an Image Edit, while adding multiple images performs an Image Combine."
251
+ supported_tasks:
252
+ - txt2img
253
+ - img2img
254
+ - inpaint
255
+ - outpaint
256
+ - hires_fix
257
+ max_count: 10
258
+ usage_guideline: "Supply reference image(s) encoded as Base64 Data URI or HTTP/HTTPS URL. Passing a single reference image performs an Image Edit, while passing multiple images (up to 10) performs an Image Combine."
259
+ parameters_schema:
260
+ type: object
261
+ properties:
262
+ image:
263
+ type: string
264
+ description: "Reference image encoded as Base64 Data URI or HTTP/HTTPS URL."
265
+ required:
266
+ - image
267
+
268
+ mage_flow_reference_edit:
269
+ chains: reference_image
270
+ display_name: "Mage-Flow Reference Edit"
271
+ description: " (Mage-Flow-Edit-Turbo/Mage-Flow-Edit recommended) For multimodal models, this feature enables powerful editing and combining capabilities. In txt2img mode, adding a single reference image performs an Image Edit, while adding multiple images performs an Image Combine."
272
+ supported_tasks:
273
+ - txt2img
274
+ - img2img
275
+ - inpaint
276
+ - outpaint
277
+ - hires_fix
278
+ max_count: 10
279
+ usage_guideline: "Supply reference image as Base64 Data URI or HTTP/HTTPS URL."
280
+ parameters_schema:
281
+ type: object
282
+ properties:
283
+ image:
284
+ type: string
285
+ description: "Reference image encoded as Base64 Data URI or HTTP/HTTPS URL."
286
+ required:
287
+ - image
288
+
289
+ krea2_identity_edit:
290
+ chains: krea2_identity_edit
291
+ display_name: "KREA2 Identity Edit"
292
+ description: "Processed using the lbouaraba/comfyui-krea2edit node. (Krea-2-Turbo recommended, Krea-2-Raw need set ZeroGPU Duration (s) to 120 ) In txt2img mode, adding a single reference image performs an Image Edit, while adding multiple images performs an Image Combine."
293
+ supported_tasks:
294
+ - txt2img
295
+ - img2img
296
+ - inpaint
297
+ - outpaint
298
+ - hires_fix
299
+ max_count: 2
300
+ usage_guideline: "Supply reference image as Base64 Data URI or HTTP/HTTPS URL."
301
+ parameters_schema:
302
+ type: object
303
+ properties:
304
+ image:
305
+ type: string
306
+ description: "Reference image encoded as Base64 Data URI or HTTP/HTTPS URL."
307
+ required:
308
+ - image
309
+
310
+ krea2_style_reference:
311
+ chains: krea2_style_reference
312
+ display_name: "KREA2 Style Reference"
313
+ description: "(Krea-2-Turbo recommended) Add style reference images to perform style reference editing."
314
+ supported_tasks:
315
+ - txt2img
316
+ - img2img
317
+ - inpaint
318
+ - outpaint
319
+ - hires_fix
320
+ max_count: 3
321
+ usage_guideline: "Supply style reference image as Base64 Data URI or HTTP/HTTPS URL."
322
+ parameters_schema:
323
+ type: object
324
+ properties:
325
+ image:
326
+ type: string
327
+ description: "Style reference image encoded as Base64 Data URI or HTTP/HTTPS URL."
328
+ required:
329
+ - image
330
+
331
+ diffsynth_controlnet:
332
+ chains: diffsynth_controlnet
333
+ display_name: "DiffSynth ControlNet"
334
+ description: "DiffSynth optimized ControlNet injector for Z-Image models."
335
+ supported_tasks:
336
+ - txt2img
337
+ - img2img
338
+ - inpaint
339
+ - outpaint
340
+ - hires_fix
341
+ max_count: 5
342
+ usage_guideline: "Supply ControlNet type (e.g., 'Canny'), series (e.g., 'alibaba-pai Controlnet Union 2.1 8steps'), pre-processed control image (Base64 Data URI or HTTP/HTTPS URL; system does NOT pre-process raw RGB images), and optional strength."
343
+ parameters_schema:
344
+ type: object
345
+ properties:
346
+ type:
347
+ type: string
348
+ description: "ControlNet conditioning type."
349
+ series:
350
+ type: string
351
+ description: "ControlNet model series name."
352
+ image:
353
+ type: string
354
+ description: "Pre-processed control image (e.g., Depth map) encoded as Base64 Data URI or HTTP/HTTPS URL. Note: System does NOT automatically pre-process raw RGB images."
355
+ strength:
356
+ type: number
357
+ default: 1.0
358
+ description: "Control influence strength."
359
+ required:
360
+ - type
361
+ - series
362
+ - image
363
+
364
+ boogu_image_edit:
365
+ chains: boogu_image_edit
366
+ display_name: "Boogu-Image Edit"
367
+ description: " (Boogu-Image-Edit-Turbo/Boogu-Image-Edit recommended, Boogu-Image-Edit need set ZeroGPU Duration (s) to 120 ) In txt2img mode, adding a single reference image performs an Image Edit, while adding multiple images performs an Image Combine."
368
+ supported_tasks:
369
+ - txt2img
370
+ - img2img
371
+ - inpaint
372
+ - outpaint
373
+ - hires_fix
374
+ max_count: 2
375
+ usage_guideline: "Supply reference image as Base64 Data URI or HTTP/HTTPS URL."
376
+ parameters_schema:
377
+ type: object
378
+ properties:
379
+ image:
380
+ type: string
381
+ description: "Reference image encoded as Base64 Data URI or HTTP/HTTPS URL."
382
+ required:
383
+ - image
384
+
385
+ joyai_reference_edit:
386
+ chains: joyai_image
387
+ display_name: "JoyAI Reference Edit"
388
+ description: " (JoyAI-Image-Edit recommended) For multimodal models, this feature enables powerful editing and combining capabilities. In txt2img mode, adding a single reference image performs an Image Edit (JoyAI-Image-Edit recommended), while adding multiple images performs an Image Combine (JoyAI-Image-Edit-Plus recommended with ZeroGPU Duration (s) set to 120)."
389
+ supported_tasks:
390
+ - txt2img
391
+ - img2img
392
+ - inpaint
393
+ - outpaint
394
+ - hires_fix
395
+ max_count: 2
396
+ usage_guideline: "Supply JoyAI reference image as Base64 Data URI or HTTP/HTTPS URL."
397
+ parameters_schema:
398
+ type: object
399
+ properties:
400
+ image:
401
+ type: string
402
+ description: "Input reference image encoded as Base64 Data URI or HTTP/HTTPS URL."
403
+ required:
404
+ - image
405
+
406
+ qwen_image_edit:
407
+ chains: qwen_image_edit
408
+ display_name: "Qwen-Image Edit"
409
+ description: " (lightx2v/Qwen-Image-Edit-2511-Lightning recommended) In txt2img mode, adding a single reference image performs an Image Edit, while adding multiple images performs an Image Combine."
410
+ supported_tasks:
411
+ - txt2img
412
+ - img2img
413
+ - inpaint
414
+ - outpaint
415
+ - hires_fix
416
+ max_count: 3
417
+ usage_guideline: "Supply reference image(s) (up to 3) encoded as Base64 Data URI or HTTP/HTTPS URL. Passing a single reference image performs an Image Edit, while passing multiple images performs an Image Combine."
418
+ parameters_schema:
419
+ type: object
420
+ properties:
421
+ image:
422
+ type: string
423
+ description: "Reference image encoded as Base64 Data URI or HTTP/HTTPS URL."
424
+ required:
425
+ - image
426
+
427
+ hidream_o1_smoothing:
428
+ chains: hidream_o1_smoothing
429
+ display_name: "HiDream O1 Smoothing Injector"
430
+ description: "HiDream O1 detail smoothing and artifact reduction injector."
431
+ supported_tasks:
432
+ - txt2img
433
+ - img2img
434
+ - inpaint
435
+ - outpaint
436
+ - hires_fix
437
+ max_count: 1
438
+ usage_guideline: "Configures smoothing factor for HiDream models."
439
+ parameters_schema:
440
+ type: object
441
+ properties:
442
+ factor:
443
+ type: number
444
+ default: 0.5
445
+ description: "Smoothing intensity (0.0 to 1.0)."
446
+ required: []
447
+
448
+ krea2_controlnet:
449
+ chains: krea2_controlnet
450
+ display_name: "KREA2 ControlNet"
451
+ description: "Processed using the facok/comfyui-krea2-controlnet node."
452
+ supported_tasks:
453
+ - txt2img
454
+ - img2img
455
+ - inpaint
456
+ - outpaint
457
+ - hires_fix
458
+ max_count: 5
459
+ usage_guideline: "Supply ControlNet type (e.g., 'Depth'), series (e.g., 'Patil'), pre-processed control image (Base64 Data URI or HTTP/HTTPS URL; system does NOT pre-process raw RGB images), and optional strength."
460
+ parameters_schema:
461
+ type: object
462
+ properties:
463
+ type:
464
+ type: string
465
+ enum:
466
+ - "Depth"
467
+ description: "ControlNet conditioning type."
468
+ series:
469
+ type: string
470
+ enum:
471
+ - "Patil"
472
+ default: "Patil"
473
+ description: "ControlNet model series."
474
+ image:
475
+ type: string
476
+ description: "Pre-processed control image (e.g., Depth map) encoded as Base64 Data URI or HTTP/HTTPS URL. Note: System does NOT automatically pre-process raw RGB images."
477
+ strength:
478
+ type: number
479
+ default: 1.0
480
+ minimum: 0.0
481
+ maximum: 2.0
482
+ description: "Control influence strength (0.0 to 2.0)."
483
+ required:
484
+ - type
485
+ - series
486
+ - image
487
+
488
+ anima_controlnet_lllite:
489
+ chains: anima_controlnet_lllite
490
+ display_name: "Anima ControlNet LLLite"
491
+ description: "Anima model-specific lightweight ControlNet."
492
+ supported_tasks:
493
+ - txt2img
494
+ - img2img
495
+ - inpaint
496
+ - outpaint
497
+ - hires_fix
498
+ max_count: 5
499
+ usage_guideline: "Supply Anima ControlNet LLLite type (e.g., 'Depth'), series (e.g., 'kohya-ss'), pre-processed control image (Base64 Data URI or HTTP/HTTPS URL; system does NOT pre-process raw RGB images), and optional strength."
500
+ parameters_schema:
501
+ type: object
502
+ properties:
503
+ type:
504
+ type: string
505
+ description: "Anima ControlNet LLLite conditioning type."
506
+ series:
507
+ type: string
508
+ description: "Anima ControlNet LLLite model series."
509
+ image:
510
+ type: string
511
+ description: "Pre-processed control image (e.g., Depth, Lineart map) encoded as Base64 Data URI or HTTP/HTTPS URL. Note: System does NOT automatically pre-process raw RGB images."
512
+ strength:
513
+ type: number
514
+ default: 1.0
515
+ minimum: 0.0
516
+ maximum: 2.0
517
+ description: "Control influence strength (0.0 to 2.0)."
518
+ required:
519
+ - type
520
+ - series
521
+ - image
522
+
523
+ hidream_o1_reference:
524
+ chains: hidream_o1_reference
525
+ display_name: "HiDream-O1 Reference Edit"
526
+ description: " (HiDream-O1-Image-Dev recommended with resolution set to 4.0MP, e.g., 2048x2048) For HiDream-O1 models, this feature enables reference image editing and combining capabilities. In txt2img mode, adding a single reference image performs an Image Edit, while adding multiple images performs an Image Combine."
527
+ supported_tasks:
528
+ - txt2img
529
+ - img2img
530
+ - inpaint
531
+ - outpaint
532
+ - hires_fix
533
+ max_count: 9
534
+ usage_guideline: "Supply reference image(s) (up to 9) encoded as Base64 Data URI or HTTP/HTTPS URL. Passing a single reference image performs an Image Edit, while passing multiple images performs an Image Combine."
535
+ parameters_schema:
536
+ type: object
537
+ properties:
538
+ image:
539
+ type: string
540
+ description: "Reference image encoded as Base64 Data URI or HTTP/HTTPS URL."
541
+ required:
542
+ - image
543
+
544
+ flux1_ipadapter:
545
+ chains: flux1_ipadapter
546
+ display_name: "Flux1 IP-Adapter"
547
+ description: "FLUX.1 model-specific IP-Adapter implementation."
548
+ supported_tasks:
549
+ - txt2img
550
+ - img2img
551
+ - inpaint
552
+ - outpaint
553
+ - hires_fix
554
+ max_count: 5
555
+ usage_guideline: "Supply reference image (Base64 Data URI or HTTP/HTTPS URL), optional weight (default 1.0), start_at (default 0.0), and end_at (default 1.0). Up to 5 images supported."
556
+ parameters_schema:
557
+ type: object
558
+ properties:
559
+ image:
560
+ type: string
561
+ description: "Reference image encoded as Base64 Data URI or HTTP/HTTPS URL."
562
+ weight:
563
+ type: number
564
+ default: 1.0
565
+ description: "Influence weight of the image prompt (0.0 to 2.0)."
566
+ start_at:
567
+ type: number
568
+ default: 0.0
569
+ description: "Start step percentage for IP-Adapter application (0.0 to 1.0)."
570
+ end_at:
571
+ type: number
572
+ default: 1.0
573
+ description: "End step percentage for IP-Adapter application (0.0 to 1.0)."
574
+ start_percent:
575
+ type: number
576
+ default: 0.0
577
+ description: "Alias for start_at."
578
+ end_percent:
579
+ type: number
580
+ default: 1.0
581
+ description: "Alias for end_at."
582
+ required:
583
+ - image
584
+
585
+ sd3_ipadapter:
586
+ chains: sd3_ipadapter
587
+ display_name: "SD3 IP-Adapter"
588
+ description: "SD3/SD3.5 model-specific IP-Adapter implementation."
589
+ supported_tasks:
590
+ - txt2img
591
+ - img2img
592
+ - inpaint
593
+ - outpaint
594
+ - hires_fix
595
+ max_count: 5
596
+ usage_guideline: "Supply reference image (Base64 Data URI or HTTP/HTTPS URL), optional weight (default 1.0), start_at (default 0.0), and end_at (default 1.0). Up to 5 images supported."
597
+ parameters_schema:
598
+ type: object
599
+ properties:
600
+ image:
601
+ type: string
602
+ description: "Reference image encoded as Base64 Data URI or HTTP/HTTPS URL."
603
+ weight:
604
+ type: number
605
+ default: 1.0
606
+ description: "Influence weight of the image prompt (0.0 to 2.0)."
607
+ start_at:
608
+ type: number
609
+ default: 0.0
610
+ description: "Start step percentage for IP-Adapter application (0.0 to 1.0)."
611
+ end_at:
612
+ type: number
613
+ default: 1.0
614
+ description: "End step percentage for IP-Adapter application (0.0 to 1.0)."
615
+ start_percent:
616
+ type: number
617
+ default: 0.0
618
+ description: "Alias for start_at."
619
+ end_percent:
620
+ type: number
621
+ default: 1.0
622
+ description: "Alias for end_at."
623
+ required:
624
+ - image
625
+
626
+ embedding:
627
+ chains: embedding
628
+ display_name: "Textual Inversion Embedding Injector"
629
+ description: "Downloads Textual Inversion embedding files from Civitai or Hugging Face to the server. Note: This feature ONLY handles file downloading/preparation. To activate the embedding, manually add 'embedding:<filename>' (e.g. 'embedding:civitai_456' for Civitai ID 456, or 'embedding:filename' for Hugging Face) into your prompt or negative_prompt."
630
+ supported_tasks:
631
+ - txt2img
632
+ - img2img
633
+ - inpaint
634
+ - outpaint
635
+ - hires_fix
636
+ max_count: 5
637
+ usage_guideline: "Specify source ('Civitai' or 'Hugging Face'), and embedding_value (Civitai Version ID or HF repo file path). The file is downloaded to server; manually enter 'embedding:<filename>' in prompt or negative_prompt to activate."
638
+ parameters_schema:
639
+ type: object
640
+ properties:
641
+ source:
642
+ type: string
643
+ enum:
644
+ - "Civitai"
645
+ - "Hugging Face"
646
+ description: "Download source for the Textual Inversion embedding file. Use 'Civitai' to download by Version ID, or 'Hugging Face' to download by repo file path."
647
+ embedding_value:
648
+ type: string
649
+ description: "For Civitai: the Version ID (e.g., '456' from civitai.com/models/123?modelVersionId=456, saved as 'civitai_456.safetensors'). For Hugging Face: repo_id/filename.extension (e.g., 'ilikebigturtles/lazypos/lazypos.safetensors', saved as 'lazypos.safetensors'). Manually reference embedding:<filename> in prompt or negative_prompt."
650
+ required:
651
+ - source
652
+ - embedding_value