@qingflow-tech/qingflow-app-builder-mcp 1.0.39 → 1.0.40

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -3,13 +3,13 @@
3
3
  Install:
4
4
 
5
5
  ```bash
6
- npm install @qingflow-tech/qingflow-app-builder-mcp@1.0.39
6
+ npm install @qingflow-tech/qingflow-app-builder-mcp@1.0.40
7
7
  ```
8
8
 
9
9
  Run:
10
10
 
11
11
  ```bash
12
- npx -y -p @qingflow-tech/qingflow-app-builder-mcp@1.0.39 qingflow-app-builder-mcp
12
+ npx -y -p @qingflow-tech/qingflow-app-builder-mcp@1.0.40 qingflow-app-builder-mcp
13
13
  ```
14
14
 
15
15
  Environment:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qingflow-tech/qingflow-app-builder-mcp",
3
- "version": "1.0.39",
3
+ "version": "1.0.40",
4
4
  "description": "Builder MCP for Qingflow app/package/system design and staged solution workflows.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/pyproject.toml CHANGED
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "qingflow-mcp"
7
- version = "1.0.39"
7
+ version = "1.0.40"
8
8
  description = "User-authenticated MCP server for Qingflow"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -36,15 +36,15 @@ Default modeling rules:
36
36
 
37
37
  Before any builder write, classify the request:
38
38
 
39
- - **Complete system / app package**: the user asks for a system, package, workspace module set, or several related forms/apps. Use `package_apply` for the package, then one `app_schema_apply(apps=[...])` for the app shells and fields. Do not squeeze several business objects into one app.
39
+ - **Complete system / app package**: the user asks for a system, package, workspace module set, or several related forms/apps. Use `package_apply` for the package, then one `app_schema_apply(package_id=..., apps=[...])` for the app shells and fields. Do not squeeze several business objects into one app.
40
40
  - **Single app**: the user names one form/app or gives one `app_key`. Use `app_resolve`/`app_get`, then `app_schema_apply` and the app-scoped apply tools.
41
41
  - **Record/user operation**: the user wants to add, edit, delete, approve, or analyze data. Route to the record/task skills instead of builder tools.
42
42
 
43
- For complete systems, `apps[]` should use stable `client_key` values. Same-call relation fields may use `target_app_ref` for a client key or `target_app` for another app name. Prefer `target_app_ref` when names may collide.
43
+ For complete systems, `apps[]` should use stable `client_key` values. Same-call relation fields may use `target_app_ref` for a client key or `target_app` for another app name. Prefer `target_app_ref` when names may collide. Create the related apps with one multi-app schema apply; do not create each app separately just to collect app keys and then patch relations afterward.
44
44
 
45
45
  Builder schema inputs should follow agent-intuitive semantics:
46
46
 
47
- - icons may be `icon/color`, `icon_name/icon_color`, `icon_config`, or `icon: {name, color}`
47
+ - primary icon syntax is `icon + color`, for example `icon: "table", color: "blue"`; `icon_name/icon_color`, `icon_config`, and `icon: {name, color}` are compatibility aliases only
48
48
  - `single_select` / `multi_select` options may be strings or objects such as `{label, value}`; tools normalize to option labels
49
49
  - relation fields need a target plus `display_field` and `visible_fields`
50
50
  - do not create built-in system fields as form fields: `数据ID`, `编号`, `申请人`, `申请时间`, `创建人`, `创建时间`, `提交人`, `提交时间`, `更新时间`, `更新人`, `当前流程状态`, `当前处理人`, `当前处理节点`, `流程标题`. They are platform-generated; only reference supported system fields where a tool explicitly says so, such as button `source_field: "数据ID"`
@@ -67,7 +67,8 @@ Treat these as the official surface. Do not default to `package_create`, `packag
67
67
  - use `package_get(package_id=...)` to read one known package
68
68
  - use `package_apply(...)` for package creation, rename, icon, visibility, grouping, ordering, and app/portal layout
69
69
  - Multi-app schema work:
70
- - use one `app_schema_apply(apps=[...])` / CLI `builder schema apply --apps-file` when creating several apps in one package
70
+ - use one `app_schema_apply(package_id=..., apps=[...])` / CLI `builder schema apply --apps-file` when creating several apps in one package
71
+ - every `apps[]` item should carry its own `client_key`, `app_name`, `icon`, `color`, and `add_fields`
71
72
  - same-call relation fields may use `target_app_ref` to point at another `apps[].client_key`, or `target_app` to point at another `apps[].app_name`
72
73
  - App base permissions:
73
74
  - trust `app_get.editability.can_edit_app_base` for app base-info writes like app name, icon, and visibility
@@ -127,7 +128,7 @@ Treat these as the official surface. Do not default to `package_create`, `packag
127
128
  1. Trust the current MCP/session when it is already injected by the runtime; only run auth/workspace recovery after a tool explicitly reports an auth, credential, or workspace error
128
129
  2. Confirm whether the task is read-only or write-impacting
129
130
  3. Classify the build scope:
130
- - complete system/package -> `package_apply` or `package_get`, then one `app_schema_apply(apps=[...])`
131
+ - complete system/package -> `package_apply` or `package_get`, then one `app_schema_apply(package_id=..., apps=[...])`
131
132
  - single app -> `app_resolve` / `app_get`, then app-scoped apply tools
132
133
  - record/task/data request -> leave builder and use the matching record/task skill
133
134
  4. Resolve the smallest stable target:
@@ -6,7 +6,7 @@ This playbook follows the current public builder surface, not legacy package hel
6
6
  Do not use this playbook when the user is really asking for a system/package with multiple forms or modules. In that case:
7
7
 
8
8
  1. read or create the package through `package_get` / `package_apply`
9
- 2. create the related apps in one `app_schema_apply(apps=[...])` call
9
+ 2. create the related apps in one `app_schema_apply(package_id=..., apps=[...])` call
10
10
  3. keep package ownership on the public `package_id` path instead of a separate attach step
11
11
  4. add same-call relation fields with `target_app_ref` when one new app references another new app
12
12
  5. choose explicit non-template `icon + color` for each new package/app
@@ -38,7 +38,7 @@ then do not treat that as one app.
38
38
  Use this pattern instead:
39
39
 
40
40
  1. `package_get` or `package_apply(create_if_missing=true, package_name=...)`
41
- 2. for a multi-app system, run one `app_schema_apply` with `apps[]`
41
+ 2. for a multi-app system, run one `app_schema_apply` with `package_id` and `apps[]`
42
42
  3. use `apps[].client_key` plus relation field `target_app_ref` when one new app references another new app
43
43
 
44
44
  ## Example
@@ -54,7 +54,8 @@ Create a new package only after the user confirms package creation:
54
54
  "profile": "default",
55
55
  "package_name": "研发项目管理",
56
56
  "create_if_missing": true,
57
- "icon": {"name": "briefcase", "color": "azure"}
57
+ "icon": "briefcase",
58
+ "color": "azure"
58
59
  }
59
60
  }
60
61
  ```
@@ -80,7 +81,8 @@ Apply schema for a new app:
80
81
  "profile": "default",
81
82
  "app_name": "客户订单",
82
83
  "package_id": 1218950,
83
- "icon": {"name": "delivery-box-1", "color": "emerald"},
84
+ "icon": "delivery-box-1",
85
+ "color": "emerald",
84
86
  "create_if_missing": true,
85
87
  "publish": true,
86
88
  "add_fields": [
@@ -110,7 +112,8 @@ Apply schema for multiple apps in one call:
110
112
  {
111
113
  "client_key": "customer",
112
114
  "app_name": "客户",
113
- "icon": {"name": "business-personalcard", "color": "emerald"},
115
+ "icon": "business-personalcard",
116
+ "color": "emerald",
114
117
  "add_fields": [
115
118
  {"name": "客户名称", "type": "text", "required": true, "as_data_title": true}
116
119
  ]
@@ -118,7 +121,8 @@ Apply schema for multiple apps in one call:
118
121
  {
119
122
  "client_key": "order",
120
123
  "app_name": "订单",
121
- "icon": {"name": "delivery-box-1", "color": "blue"},
124
+ "icon": "delivery-box-1",
125
+ "color": "blue",
122
126
  "add_fields": [
123
127
  {"name": "订单编号", "type": "text", "required": true, "as_data_title": true},
124
128
  {
@@ -21,7 +21,7 @@ If the user asks for multiple forms/modules that relate to each other, this is a
21
21
 
22
22
  Use this split consistently:
23
23
 
24
- - Complete system/package: `package_apply` first, then one `app_schema_apply(apps=[...])`.
24
+ - Complete system/package: `package_apply` first, then one `app_schema_apply(package_id=..., apps=[...])`.
25
25
  - Single app: `app_resolve/app_get` first, then app-scoped apply tools.
26
26
  - Record/task/data operation: leave builder and use record/task skills.
27
27
 
@@ -103,4 +103,4 @@ For object-level updates, the safe partial syntax is `patch_*` with the object's
103
103
  - Do not omit assignees on approval/fill/copy nodes
104
104
  - Do not patch preset flows with brand new approval/fill node ids unless you are intentionally replacing the skeleton; reuse preset ids like `approve_1` and `fill_1`
105
105
  - Do not guess role ids, member ids, or editable field ids; resolve names first
106
- - Do not force agent-authored schema into backend-internal names when public aliases exist: icons may use `icon_config` / `icon:{name,color}`, options may use `{label,value}`, and same-call relation targets may use `target_app_ref` / `target_app`.
106
+ - Do not force agent-authored schema into backend-internal names when public keys exist: write icons as `icon + color` first; `icon_config` / `icon:{name,color}` are compatibility aliases only. Options may use `{label,value}`, and same-call relation targets may use `target_app_ref` / `target_app`.
@@ -1,10 +1,11 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import argparse
4
+ from copy import deepcopy
4
5
 
5
6
  from ..context import CliContext
6
- from .common import load_list_arg, load_object_arg, raise_config_error, require_list_arg
7
7
  from ..json_io import load_json_value
8
+ from .common import load_list_arg, load_object_arg, raise_config_error, require_list_arg
8
9
 
9
10
 
10
11
  def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
@@ -479,6 +480,7 @@ def _handle_chart_get(args: argparse.Namespace, context: CliContext) -> dict:
479
480
  def _handle_schema_apply(args: argparse.Namespace, context: CliContext) -> dict:
480
481
  apps_payload = _load_apps_file_arg(args.apps_file)
481
482
  apps = apps_payload["apps"]
483
+ apps_warnings = apps_payload.get("warnings") or []
482
484
  package_id = args.package_id
483
485
  if package_id is None and apps_payload.get("package_id") is not None:
484
486
  package_id = int(apps_payload["package_id"])
@@ -487,6 +489,7 @@ def _handle_schema_apply(args: argparse.Namespace, context: CliContext) -> dict:
487
489
  raise_config_error(
488
490
  "schema apply multi-app mode requires a non-empty --apps-file.",
489
491
  fix_hint="Pass a JSON array, or a JSON object like {\"package_id\":1001,\"apps\":[...]} with at least one app item.",
492
+ error_code="APPS_FILE_EMPTY",
490
493
  )
491
494
  if args.app_key or args.app_name or args.app_title or args.add_fields_file or args.update_fields_file or args.remove_fields_file:
492
495
  raise_config_error(
@@ -498,7 +501,7 @@ def _handle_schema_apply(args: argparse.Namespace, context: CliContext) -> dict:
498
501
  "schema apply multi-app mode requires --package-id.",
499
502
  fix_hint="Pass `--package-id`, or put `package_id` at the top level of --apps-file. `package_name` alone does not create the package here; run `builder package apply` first.",
500
503
  )
501
- return context.builder.app_schema_apply(
504
+ result = context.builder.app_schema_apply(
502
505
  profile=args.profile,
503
506
  package_id=package_id,
504
507
  visibility=load_object_arg(args.visibility_file, option_name="--visibility-file"),
@@ -509,6 +512,11 @@ def _handle_schema_apply(args: argparse.Namespace, context: CliContext) -> dict:
509
512
  update_fields=[],
510
513
  remove_fields=[],
511
514
  )
515
+ if apps_warnings and isinstance(result, dict):
516
+ result_warnings = list(result.get("warnings") or [])
517
+ result_warnings.extend(deepcopy(apps_warnings))
518
+ result["warnings"] = result_warnings
519
+ return result
512
520
  has_app_key = bool((args.app_key or "").strip())
513
521
  has_app_name = bool((args.app_name or "").strip())
514
522
  has_app_title = bool((args.app_title or "").strip())
@@ -545,8 +553,56 @@ def _handle_schema_apply(args: argparse.Namespace, context: CliContext) -> dict:
545
553
  def _load_apps_file_arg(path: str | None) -> dict[str, object]:
546
554
  if not path:
547
555
  return {"apps": []}
556
+ expected_shape = {
557
+ "package_id": 1001,
558
+ "apps": [
559
+ {
560
+ "client_key": "employee",
561
+ "app_name": "员工花名册",
562
+ "icon": "business-personalcard",
563
+ "color": "emerald",
564
+ "add_fields": [],
565
+ }
566
+ ],
567
+ }
568
+ expected_shape_details = {
569
+ "expected_shape": expected_shape,
570
+ "expected_shape_json": (
571
+ '{"package_id":1001,"apps":[{"client_key":"employee","app_name":"员工花名册",'
572
+ '"icon":"business-personalcard","color":"emerald","add_fields":[]}]}'
573
+ ),
574
+ }
548
575
  payload = load_json_value(path, option_name="--apps-file")
549
576
  if isinstance(payload, list):
577
+ if len(payload) == 1 and isinstance(payload[0], dict) and "apps" in payload[0]:
578
+ wrapper = payload[0]
579
+ apps = wrapper.get("apps")
580
+ if not isinstance(apps, list):
581
+ raise_config_error(
582
+ "--apps-file wrapper object requires an apps array.",
583
+ fix_hint="Use {\"package_id\":1001,\"apps\":[...]} or pass a raw apps array like [{\"app_name\":\"...\",\"icon\":\"...\",\"color\":\"...\",\"add_fields\":[...]}].",
584
+ error_code="APPS_FILE_SHAPE_INVALID",
585
+ details=expected_shape_details,
586
+ )
587
+ result: dict[str, object] = {
588
+ "apps": apps,
589
+ "warnings": [
590
+ {
591
+ "code": "APPS_FILE_WRAPPER_ARRAY_UNWRAPPED",
592
+ "message": "--apps-file was a singleton wrapper array; normalized it to {package_id, apps}.",
593
+ }
594
+ ],
595
+ }
596
+ if wrapper.get("package_id") is not None:
597
+ result["package_id"] = wrapper.get("package_id")
598
+ return result
599
+ if any(isinstance(item, dict) and "apps" in item for item in payload):
600
+ raise_config_error(
601
+ "--apps-file root array items must be app items, not package wrapper objects.",
602
+ fix_hint="Use one object {\"package_id\":1001,\"apps\":[...]} or a raw app array [{\"app_name\":\"...\",\"icon\":\"...\",\"color\":\"...\",\"add_fields\":[...]}]. Do not wrap multiple {package_id, apps} objects in an array.",
603
+ error_code="APPS_FILE_SHAPE_INVALID",
604
+ details=expected_shape_details,
605
+ )
550
606
  return {"apps": payload}
551
607
  if isinstance(payload, dict):
552
608
  apps = payload.get("apps")
@@ -554,6 +610,8 @@ def _load_apps_file_arg(path: str | None) -> dict[str, object]:
554
610
  raise_config_error(
555
611
  "--apps-file JSON object requires an apps array.",
556
612
  fix_hint="Use {\"package_id\":1001,\"apps\":[...]} or pass a raw JSON array.",
613
+ error_code="APPS_FILE_SHAPE_INVALID",
614
+ details=expected_shape_details,
557
615
  )
558
616
  result: dict[str, object] = {"apps": apps}
559
617
  if payload.get("package_id") is not None:
@@ -562,6 +620,8 @@ def _load_apps_file_arg(path: str | None) -> dict[str, object]:
562
620
  raise_config_error(
563
621
  "--apps-file must be a JSON array or an object containing apps.",
564
622
  fix_hint="Use [{...}] or {\"package_id\":1001,\"apps\":[...]}",
623
+ error_code="APPS_FILE_SHAPE_INVALID",
624
+ details=expected_shape_details,
565
625
  )
566
626
 
567
627
 
@@ -48,14 +48,23 @@ def read_secret_arg(value: str | None, *, stdin_enabled: bool, label: str) -> st
48
48
  raise QingflowApiError.config_error(f"{label} is required")
49
49
 
50
50
 
51
- def raise_config_error(message: str, *, fix_hint: str) -> None:
51
+ def raise_config_error(
52
+ message: str,
53
+ *,
54
+ fix_hint: str | None = None,
55
+ error_code: str = "CONFIG_ERROR",
56
+ details: dict[str, Any] | None = None,
57
+ ) -> None:
58
+ error_details: dict[str, Any] = {"fix_hint": fix_hint or message}
59
+ if details:
60
+ error_details.update(details)
52
61
  raise RuntimeError(
53
62
  json.dumps(
54
63
  {
55
64
  "category": "config",
56
65
  "message": message,
57
- "error_code": "CONFIG_ERROR",
58
- "details": {"fix_hint": fix_hint},
66
+ "error_code": error_code,
67
+ "details": error_details,
59
68
  },
60
69
  ensure_ascii=False,
61
70
  )
@@ -383,7 +383,7 @@ class AiBuilderTools(ToolBase):
383
383
  remove_fields: list[JSONObject] | None = None,
384
384
  apps: list[JSONObject] | None = None,
385
385
  ) -> JSONObject:
386
- if apps:
386
+ if apps is not None:
387
387
  if app_key or app_name or app_title or add_fields or update_fields or remove_fields:
388
388
  return _config_failure(
389
389
  tool_name="app_schema_apply",
@@ -1718,8 +1718,18 @@ class AiBuilderTools(ToolBase):
1718
1718
  icon_color=icon_color,
1719
1719
  icon_config=icon_config,
1720
1720
  )
1721
- if apps:
1722
- apps = [_normalize_schema_app_item(item) if isinstance(item, dict) else item for item in apps]
1721
+ if apps is not None:
1722
+ normalized_apps_payload = _normalize_schema_apps_argument(
1723
+ tool_name="app_schema_apply",
1724
+ package_id=package_id,
1725
+ apps=apps,
1726
+ )
1727
+ failure = normalized_apps_payload.get("failure")
1728
+ if isinstance(failure, dict):
1729
+ return _attach_builder_apply_envelope("app_schema_apply", failure)
1730
+ package_id = normalized_apps_payload.get("package_id") # type: ignore[assignment]
1731
+ apps = normalized_apps_payload.get("apps") # type: ignore[assignment]
1732
+ input_warnings = list(normalized_apps_payload.get("warnings") or [])
1723
1733
  result = self._app_schema_apply_multi(
1724
1734
  profile=profile,
1725
1735
  package_id=package_id,
@@ -1728,6 +1738,10 @@ class AiBuilderTools(ToolBase):
1728
1738
  publish=publish,
1729
1739
  apps=apps,
1730
1740
  )
1741
+ if input_warnings:
1742
+ result_warnings = list(result.get("warnings") or [])
1743
+ result_warnings.extend(deepcopy(input_warnings))
1744
+ result["warnings"] = result_warnings
1731
1745
  return _attach_builder_apply_envelope("app_schema_apply", result)
1732
1746
  result = self._app_schema_apply_once(
1733
1747
  profile=profile,
@@ -1803,9 +1817,13 @@ class AiBuilderTools(ToolBase):
1803
1817
  if not apps:
1804
1818
  return _config_failure(
1805
1819
  tool_name="app_schema_apply",
1820
+ error_code="APPS_FILE_EMPTY",
1806
1821
  message="app_schema_apply multi-app mode requires non-empty apps.",
1807
1822
  fix_hint="Pass apps as a non-empty list of app schema items.",
1808
1823
  )
1824
+ shape_failure = _schema_apps_shape_failure(tool_name="app_schema_apply", apps=apps)
1825
+ if shape_failure is not None:
1826
+ return shape_failure
1809
1827
  icon_errors: list[JSONObject] = []
1810
1828
  seen_new_app_icons: dict[str, int] = {}
1811
1829
  for index, raw_item in enumerate(apps):
@@ -2932,6 +2950,109 @@ def _normalize_schema_app_item(item: JSONObject) -> JSONObject:
2932
2950
  return normalized
2933
2951
 
2934
2952
 
2953
+ def _schema_apps_expected_shape() -> JSONObject:
2954
+ return {
2955
+ "package_id": 1001,
2956
+ "apps": [
2957
+ {
2958
+ "client_key": "employee",
2959
+ "app_name": "员工花名册",
2960
+ "icon": "business-personalcard",
2961
+ "color": "emerald",
2962
+ "add_fields": [{"name": "员工名称", "type": "text", "as_data_title": True}],
2963
+ }
2964
+ ],
2965
+ }
2966
+
2967
+
2968
+ def _schema_apps_expected_shape_json() -> str:
2969
+ return (
2970
+ '{"package_id":1001,"apps":[{"client_key":"employee","app_name":"员工花名册",'
2971
+ '"icon":"business-personalcard","color":"emerald","add_fields":[{"name":"员工名称","type":"text","as_data_title":true}]}]}'
2972
+ )
2973
+
2974
+
2975
+ def _is_schema_apps_wrapper_item(item: object) -> bool:
2976
+ return isinstance(item, dict) and "apps" in item
2977
+
2978
+
2979
+ def _schema_apps_shape_failure(*, tool_name: str, apps: list[JSONObject]) -> JSONObject | None:
2980
+ for index, item in enumerate(apps):
2981
+ if _is_schema_apps_wrapper_item(item):
2982
+ return _config_failure(
2983
+ tool_name=tool_name,
2984
+ error_code="APPS_FILE_SHAPE_INVALID",
2985
+ message="apps[] items must be app schema items, not package wrapper objects.",
2986
+ fix_hint=(
2987
+ "For MCP pass package_id separately and apps=[{app_name, icon, color, add_fields}]. "
2988
+ 'For CLI use --apps-file with {"package_id":1001,"apps":[...]}. '
2989
+ "Do not pass multiple {package_id, apps} wrapper objects inside apps[]."
2990
+ ),
2991
+ details={
2992
+ "index": index,
2993
+ "row_number": index + 1,
2994
+ "expected_shape": _schema_apps_expected_shape(),
2995
+ "expected_shape_json": _schema_apps_expected_shape_json(),
2996
+ },
2997
+ )
2998
+ return None
2999
+
3000
+
3001
+ def _normalize_schema_apps_argument(*, tool_name: str, package_id: int | None, apps: list[JSONObject]) -> JSONObject:
3002
+ normalized_package_id = package_id
3003
+ normalized_apps: list[JSONObject] = apps
3004
+ warnings: list[JSONObject] = []
3005
+
3006
+ if len(apps) == 1 and _is_schema_apps_wrapper_item(apps[0]):
3007
+ wrapper = apps[0]
3008
+ wrapper_apps = wrapper.get("apps")
3009
+ if not isinstance(wrapper_apps, list):
3010
+ return {
3011
+ "failure": _config_failure(
3012
+ tool_name=tool_name,
3013
+ error_code="APPS_FILE_SHAPE_INVALID",
3014
+ message="apps singleton wrapper requires an apps array.",
3015
+ fix_hint='Use {"package_id":1001,"apps":[...]} in CLI, or pass MCP package_id=1001 and apps=[...].',
3016
+ details={
3017
+ "expected_shape": _schema_apps_expected_shape(),
3018
+ "expected_shape_json": _schema_apps_expected_shape_json(),
3019
+ },
3020
+ )
3021
+ }
3022
+ if normalized_package_id is None and wrapper.get("package_id") is not None:
3023
+ try:
3024
+ normalized_package_id = int(wrapper.get("package_id"))
3025
+ except (TypeError, ValueError):
3026
+ return {
3027
+ "failure": _config_failure(
3028
+ tool_name=tool_name,
3029
+ error_code="APPS_FILE_SHAPE_INVALID",
3030
+ message="apps singleton wrapper package_id must be an integer.",
3031
+ fix_hint="Pass package_id as a number at the top level.",
3032
+ details={
3033
+ "expected_shape": _schema_apps_expected_shape(),
3034
+ "expected_shape_json": _schema_apps_expected_shape_json(),
3035
+ },
3036
+ )
3037
+ }
3038
+ normalized_apps = wrapper_apps
3039
+ warnings.append(
3040
+ {
3041
+ "code": "APPS_FILE_WRAPPER_ARRAY_UNWRAPPED",
3042
+ "message": "apps was a singleton wrapper array; normalized it to package_id + apps.",
3043
+ }
3044
+ )
3045
+ else:
3046
+ failure = _schema_apps_shape_failure(tool_name=tool_name, apps=apps)
3047
+ if failure is not None:
3048
+ return {"failure": failure}
3049
+
3050
+ normalized_items: list[JSONObject] = []
3051
+ for item in normalized_apps:
3052
+ normalized_items.append(_normalize_schema_app_item(item) if isinstance(item, dict) else item)
3053
+ return {"package_id": normalized_package_id, "apps": normalized_items, "warnings": warnings}
3054
+
3055
+
2935
3056
  def _compile_multi_app_schema_item_refs(
2936
3057
  item: JSONObject,
2937
3058
  client_key_to_app_key: dict[str, str],
@@ -4438,7 +4559,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
4438
4559
  "execution_notes": [
4439
4560
  "create or update package metadata, visibility, grouping, and ordering in one call",
4440
4561
  "creating a package requires explicit icon + color; icon=template is blocked because it is too generic",
4441
- "icon can be passed as icon/color, icon_name/icon_color, icon_config, or icon={name,color}; all forms normalize to icon/color",
4562
+ "agent-facing primary icon shape is icon + color; icon_name/icon_color, icon_config, and icon={name,color} are compatibility aliases that normalize to icon/color",
4442
4563
  "updating a package preserves existing icon/color when omitted; explicit icon/color values are still validated",
4443
4564
  "call workspace_icon_catalog_get or `qingflow --json builder icon catalog` for supported icon/color candidates",
4444
4565
  "metadata keys omitted on update are preserved",
@@ -5024,12 +5145,13 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
5024
5145
  "create mode: package_id + app_name + create_if_missing=true",
5025
5146
  "create mode follows backend CreateAppBean: package add_app permission is checked on the target package; package edit_app is not required for the create precheck",
5026
5147
  "multi-app mode: pass package_id + create_if_missing + apps[]; do not mix apps with top-level app_key/app_name/add_fields/update_fields/remove_fields",
5148
+ "CLI --apps-file primary shape is {package_id, apps:[...]}; raw app arrays and singleton wrapper arrays are compatibility paths, not recommended examples",
5027
5149
  "multi-app relation fields may use target_app_ref to point at another apps[].client_key; the tool creates/resolves app shells first and compiles it to target_app_key",
5028
5150
  "multi-app relation fields may also use target_app with another apps[].app_name; prefer target_app_ref/client_key when names may collide",
5029
5151
  "multi-app mode is not transactional; read created_app_keys and apps[].status before retrying, and retry only failed app items",
5030
5152
  "create mode defaults new app visibility to workspace/not when visibility is omitted; edit mode preserves current visibility when omitted",
5031
5153
  "create mode requires explicit icon + color; icon=template is blocked because it is too generic",
5032
- "icon can be passed as icon/color, icon_name/icon_color, icon_config, or icon={name,color}; all forms normalize to icon/color",
5154
+ "agent-facing primary icon shape is icon + color; icon_name/icon_color, icon_config, and icon={name,color} are compatibility aliases that normalize to icon/color",
5033
5155
  "multi-app create mode requires each new app item to include a distinct non-template icon and a valid color",
5034
5156
  "single_select and multi_select options accept strings or objects such as {label,value}; builder normalizes them to option labels before writing",
5035
5157
  "edit mode preserves existing icon/color when omitted; explicit icon/color values are still validated",