@qingflow-tech/qingflow-app-user-mcp 1.0.32 → 1.0.34

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-user-mcp@1.0.32
6
+ npm install @qingflow-tech/qingflow-app-user-mcp@1.0.34
7
7
  ```
8
8
 
9
9
  Run:
10
10
 
11
11
  ```bash
12
- npx -y -p @qingflow-tech/qingflow-app-user-mcp@1.0.32 qingflow-app-user-mcp
12
+ npx -y -p @qingflow-tech/qingflow-app-user-mcp@1.0.34 qingflow-app-user-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-user-mcp",
3
- "version": "1.0.32",
3
+ "version": "1.0.34",
4
4
  "description": "Operational end-user MCP for Qingflow records, tasks, comments, and directory 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.32"
7
+ version = "1.0.34"
8
8
  description = "User-authenticated MCP server for Qingflow"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -32,6 +32,22 @@ Default modeling rules:
32
32
  - Another business object -> a separate app, not a text field
33
33
  - Cross-object links -> relation fields, not text fields
34
34
 
35
+ ## Route First
36
+
37
+ Before any builder write, classify the request:
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.
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
+ - **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
+
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.
44
+
45
+ Builder schema inputs should follow agent-intuitive semantics:
46
+
47
+ - icons may be `icon/color`, `icon_name/icon_color`, `icon_config`, or `icon: {name, color}`
48
+ - `single_select` / `multi_select` options may be strings or objects such as `{label, value}`; tools normalize to option labels
49
+ - relation fields need a target plus `display_field` and `visible_fields`
50
+
35
51
  ## Public Tools You Should Think In
36
52
 
37
53
  - Package: `package_get`, `package_apply`
@@ -51,7 +67,7 @@ Treat these as the official surface. Do not default to `package_create`, `packag
51
67
  - use `package_apply(...)` for package creation, rename, icon, visibility, grouping, ordering, and app/portal layout
52
68
  - Multi-app schema work:
53
69
  - use one `app_schema_apply(apps=[...])` / CLI `builder schema apply --apps-file` when creating several apps in one package
54
- - same-call relation fields may use `target_app_ref` to point at another `apps[].client_key`
70
+ - 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`
55
71
  - App base permissions:
56
72
  - trust `app_get.editability.can_edit_app_base` for app base-info writes like app name, icon, and visibility
57
73
  - `can_edit_form` only means schema/form-route capability; it no longer implies app base-info write capability
@@ -107,11 +123,15 @@ Treat these as the official surface. Do not default to `package_create`, `packag
107
123
 
108
124
  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
109
125
  2. Confirm whether the task is read-only or write-impacting
110
- 3. Resolve the smallest stable target:
126
+ 3. Classify the build scope:
127
+ - complete system/package -> `package_apply` or `package_get`, then one `app_schema_apply(apps=[...])`
128
+ - single app -> `app_resolve` / `app_get`, then app-scoped apply tools
129
+ - record/task/data request -> leave builder and use the matching record/task skill
130
+ 4. Resolve the smallest stable target:
111
131
  - app-scoped work -> `app_resolve`
112
132
  - package-scoped work with known id -> `package_get`
113
133
  - portal inventory -> `portal_list`
114
- 4. Read only the smallest config slice needed:
134
+ 5. Read only the smallest config slice needed:
115
135
  - app map -> `app_get` (default entry; includes compact views, charts, custom buttons, and associated resource pool)
116
136
  - fields -> `app_get_fields`
117
137
  - layout -> `app_get_layout`
@@ -119,8 +139,8 @@ Treat these as the official surface. Do not default to `package_create`, `packag
119
139
  - flow -> `app_get_flow`
120
140
  - charts -> `app_get_charts` only when the app_get compact list is not enough
121
141
  - portal -> `portal_get`
122
- 5. If the public shape is unclear, call `builder_tool_contract`
123
- 6. Apply the smallest patch tool that fits:
142
+ 6. If the public shape is unclear, call `builder_tool_contract`
143
+ 7. Apply the smallest patch tool that fits:
124
144
  - fields -> `app_schema_apply`
125
145
  - layout -> `app_layout_apply`
126
146
  - flow -> `app_flow_apply`
@@ -130,7 +150,7 @@ Treat these as the official surface. Do not default to `package_create`, `packag
130
150
  - existing charts -> `app_charts_apply.patch_charts`; new/full charts -> `app_charts_apply.upsert_charts`
131
151
  - portal -> `portal_apply`
132
152
  - package metadata/layout -> `package_apply`
133
- 7. Use `app_publish_verify` only when the user explicitly wants final publish/live verification or you need a dedicated verification pass
153
+ 8. Use `app_publish_verify` only when the user explicitly wants final publish/live verification or you need a dedicated verification pass
134
154
 
135
155
  ## Safe Usage Rules
136
156
 
@@ -9,6 +9,7 @@ Do not use this playbook when the user is really asking for a system/package wit
9
9
  2. create the related apps in one `app_schema_apply(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
+ 5. choose explicit non-template `icon + color` for each new package/app
12
13
 
13
14
  Hierarchy reminder:
14
15
 
@@ -50,7 +51,8 @@ Create a new package only after the user confirms package creation:
50
51
  "arguments": {
51
52
  "profile": "default",
52
53
  "package_name": "研发项目管理",
53
- "create_if_missing": true
54
+ "create_if_missing": true,
55
+ "icon": {"name": "briefcase", "color": "azure"}
54
56
  }
55
57
  }
56
58
  ```
@@ -76,6 +78,7 @@ Apply schema for a new app:
76
78
  "profile": "default",
77
79
  "app_name": "客户订单",
78
80
  "package_id": 1218950,
81
+ "icon": {"name": "delivery-box-1", "color": "emerald"},
79
82
  "create_if_missing": true,
80
83
  "publish": true,
81
84
  "add_fields": [
@@ -83,7 +86,7 @@ Apply schema for a new app:
83
86
  {"name": "客户名称", "type": "text", "required": true},
84
87
  {"name": "订单封面", "type": "attachment", "as_data_cover": true},
85
88
  {"name": "订单金额", "type": "amount"},
86
- {"name": "状态", "type": "single_select", "options": ["草稿", "进行中", "已完成"], "required": true}
89
+ {"name": "状态", "type": "single_select", "options": [{"label": "草稿"}, {"label": "进行中"}, {"label": "已完成"}], "required": true}
87
90
  ],
88
91
  "update_fields": [],
89
92
  "remove_fields": []
@@ -105,6 +108,7 @@ Apply schema for multiple apps in one call:
105
108
  {
106
109
  "client_key": "customer",
107
110
  "app_name": "客户",
111
+ "icon": {"name": "business-personalcard", "color": "emerald"},
108
112
  "add_fields": [
109
113
  {"name": "客户名称", "type": "text", "required": true, "as_data_title": true}
110
114
  ]
@@ -112,6 +116,7 @@ Apply schema for multiple apps in one call:
112
116
  {
113
117
  "client_key": "order",
114
118
  "app_name": "订单",
119
+ "icon": {"name": "delivery-box-1", "color": "blue"},
115
120
  "add_fields": [
116
121
  {"name": "订单编号", "type": "text", "required": true, "as_data_title": true},
117
122
  {
@@ -19,6 +19,12 @@ Before picking tools, decide which layer the request targets:
19
19
 
20
20
  If the user asks for multiple forms/modules that relate to each other, this is a package-level multi-app task, not a single-app create.
21
21
 
22
+ Use this split consistently:
23
+
24
+ - Complete system/package: `package_apply` first, then one `app_schema_apply(apps=[...])`.
25
+ - Single app: `app_resolve/app_get` first, then app-scoped apply tools.
26
+ - Record/task/data operation: leave builder and use record/task skills.
27
+
22
28
  ## Resolve
23
29
 
24
30
  - `package_get`: read one known package by `package_id`; it reads package `baseInfo` first and may warn `PACKAGE_DETAIL_READ_DEGRADED` when the richer detail endpoint needs package edit/add-app permission. Treat that warning as degraded detail, not as package-read failure.
@@ -65,7 +71,7 @@ For object-level updates, the safe partial syntax is `patch_*` with the object's
65
71
  - Create a brand new package, then create one app in it:
66
72
  `package_apply(create_if_missing=true) -> app_schema_apply`
67
73
  - Create a brand new multi-app system/package:
68
- `package_apply(create_if_missing=true) -> app_schema_apply(apps[])`
74
+ `package_apply(create_if_missing=true) -> app_schema_apply(apps[])`; use `apps[].client_key` plus `target_app_ref`, or `target_app` when referencing by app name
69
75
  - Update fields on an existing app:
70
76
  `app_resolve -> app_get_fields -> app_schema_apply`
71
77
  - Tidy layout:
@@ -97,3 +103,4 @@ For object-level updates, the safe partial syntax is `patch_*` with the object's
97
103
  - Do not omit assignees on approval/fill/copy nodes
98
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`
99
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`.
@@ -62,7 +62,7 @@ Use one of these two modes:
62
62
  2. Discover the exact target with `task_list`
63
63
  3. If the target or action requirements are ambiguous, read `task_get`; otherwise go straight to `task_action_execute`
64
64
  4. Execute through `task_action_execute`
65
- 5. After actions, report the exact `app_key`, `record_id`, `workflow_node_id`, executed action, and any warnings
65
+ 5. After actions, report whether it succeeded, the `task_id`, the executed action, the final route/status, and any warnings
66
66
 
67
67
  ## Task-Center Rules
68
68
 
@@ -111,7 +111,7 @@ Use one of these two modes:
111
111
  - Do not execute `task_action_execute` until the user explicitly confirms the chosen action
112
112
  - Exception: if the user has already explicitly authorized a concrete action on exact targets, you may execute directly after exact target resolution
113
113
  - Avoid actions on ambiguous tasks or records
114
- - Summarize the final action and the exact `app_key / record_id / workflow_node_id`
114
+ - Summarize the final action by `task_id`; include `app_key`, `record_id`, or `workflow_node_id` only as read-only context when the tool returns them, not as the action locator
115
115
  - `reject` requires `payload.audit_feedback`
116
116
  - For approve/reject, trust the current task detail or an explicit frontend-provided `formId`; app baseInfo is only a fallback. A baseInfo `40002` is not final task-action denial when `formId` is already known.
117
117
  - `save_only` requires non-empty `fields` and is only available when the backend exposes editable fields for the current node
@@ -2292,6 +2292,8 @@ def _normalize_field_payload(value: Any) -> Any:
2292
2292
  payload = dict(value)
2293
2293
  if "fields" in payload and "subfields" not in payload:
2294
2294
  payload["subfields"] = payload.pop("fields")
2295
+ if "options" in payload:
2296
+ payload["options"] = _normalize_field_options(payload.get("options"))
2295
2297
  raw_type = payload.get("type")
2296
2298
  if isinstance(raw_type, int):
2297
2299
  normalized_from_id = FIELD_TYPE_ID_ALIASES.get(raw_type)
@@ -2324,6 +2326,34 @@ def _normalize_field_payload(value: Any) -> Any:
2324
2326
  return payload
2325
2327
 
2326
2328
 
2329
+ def _normalize_field_options(value: Any) -> Any:
2330
+ if not isinstance(value, list):
2331
+ return value
2332
+ normalized: list[str] = []
2333
+ for item in value:
2334
+ if isinstance(item, str):
2335
+ normalized.append(item)
2336
+ continue
2337
+ if isinstance(item, (int, float, bool)):
2338
+ normalized.append(str(item))
2339
+ continue
2340
+ if isinstance(item, dict):
2341
+ label = (
2342
+ item.get("label")
2343
+ or item.get("name")
2344
+ or item.get("title")
2345
+ or item.get("value")
2346
+ or item.get("text")
2347
+ or item.get("optValue")
2348
+ or item.get("optName")
2349
+ )
2350
+ if label is not None:
2351
+ normalized.append(str(label))
2352
+ continue
2353
+ normalized.append(str(item))
2354
+ return normalized
2355
+
2356
+
2327
2357
  def _slugify_title(title: str) -> str:
2328
2358
  normalized = "".join(ch.lower() if ch.isalnum() else "_" for ch in str(title or ""))
2329
2359
  collapsed = "_".join(part for part in normalized.split("_") if part)
@@ -4,6 +4,7 @@ import argparse
4
4
 
5
5
  from ..context import CliContext
6
6
  from .common import load_list_arg, load_object_arg, raise_config_error, require_list_arg
7
+ from ..json_io import load_json_value
7
8
 
8
9
 
9
10
  def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
@@ -476,26 +477,30 @@ def _handle_chart_get(args: argparse.Namespace, context: CliContext) -> dict:
476
477
 
477
478
 
478
479
  def _handle_schema_apply(args: argparse.Namespace, context: CliContext) -> dict:
479
- apps = load_list_arg(args.apps_file, option_name="--apps-file")
480
+ apps_payload = _load_apps_file_arg(args.apps_file)
481
+ apps = apps_payload["apps"]
482
+ package_id = args.package_id
483
+ if package_id is None and apps_payload.get("package_id") is not None:
484
+ package_id = int(apps_payload["package_id"])
480
485
  if args.apps_file:
481
486
  if not apps:
482
487
  raise_config_error(
483
488
  "schema apply multi-app mode requires a non-empty --apps-file.",
484
- fix_hint="Pass a JSON array with at least one app item.",
489
+ fix_hint="Pass a JSON array, or a JSON object like {\"package_id\":1001,\"apps\":[...]} with at least one app item.",
485
490
  )
486
491
  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:
487
492
  raise_config_error(
488
493
  "schema apply multi-app mode accepts --package-id/--create-if-missing plus --apps-file only.",
489
494
  fix_hint="Use `--apps-file` for batch mode, or remove `--apps-file` and use the single-app arguments.",
490
495
  )
491
- if args.package_id is None:
496
+ if package_id is None:
492
497
  raise_config_error(
493
498
  "schema apply multi-app mode requires --package-id.",
494
- fix_hint="Pass `--package-id` and app names inside --apps-file.",
499
+ 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.",
495
500
  )
496
501
  return context.builder.app_schema_apply(
497
502
  profile=args.profile,
498
- package_id=args.package_id,
503
+ package_id=package_id,
499
504
  visibility=load_object_arg(args.visibility_file, option_name="--visibility-file"),
500
505
  create_if_missing=bool(args.create_if_missing),
501
506
  publish=bool(args.publish),
@@ -537,6 +542,29 @@ def _handle_schema_apply(args: argparse.Namespace, context: CliContext) -> dict:
537
542
  )
538
543
 
539
544
 
545
+ def _load_apps_file_arg(path: str | None) -> dict[str, object]:
546
+ if not path:
547
+ return {"apps": []}
548
+ payload = load_json_value(path, option_name="--apps-file")
549
+ if isinstance(payload, list):
550
+ return {"apps": payload}
551
+ if isinstance(payload, dict):
552
+ apps = payload.get("apps")
553
+ if not isinstance(apps, list):
554
+ raise_config_error(
555
+ "--apps-file JSON object requires an apps array.",
556
+ fix_hint="Use {\"package_id\":1001,\"apps\":[...]} or pass a raw JSON array.",
557
+ )
558
+ result: dict[str, object] = {"apps": apps}
559
+ if payload.get("package_id") is not None:
560
+ result["package_id"] = payload.get("package_id")
561
+ return result
562
+ raise_config_error(
563
+ "--apps-file must be a JSON array or an object containing apps.",
564
+ fix_hint="Use [{...}] or {\"package_id\":1001,\"apps\":[...]}",
565
+ )
566
+
567
+
540
568
  def _handle_layout_apply(args: argparse.Namespace, context: CliContext) -> dict:
541
569
  return context.builder.app_layout_apply(
542
570
  profile=args.profile,
@@ -9,6 +9,7 @@ from ..errors import QingflowApiError, backend_code_value_int, message_looks_lik
9
9
  from ..public_surface import cli_public_tool_spec_from_namespace
10
10
  from ..response_trim import resolve_cli_tool_name, trim_error_response, trim_public_response
11
11
  from ..tools.ai_builder_tools import _attach_builder_apply_envelope
12
+ from ..version import get_cli_version
12
13
  from .context import CliContext, build_cli_context
13
14
  from .formatters import emit_json_result, emit_text_result
14
15
  from .commands import register_all_commands
@@ -34,7 +35,10 @@ def build_parser() -> argparse.ArgumentParser:
34
35
  parser = _QingflowArgumentParser(prog="qingflow", description="Qingflow CLI")
35
36
  parser.add_argument("--profile", default="default", help="会话 profile,默认 default")
36
37
  parser.add_argument("--json", action="store_true", help="输出 JSON")
38
+ parser.add_argument("--version", action="store_true", help="输出 Qingflow CLI 版本")
37
39
  subparsers = parser.add_subparsers(dest="command", required=True)
40
+ version_parser = subparsers.add_parser("version", help="输出 Qingflow CLI 版本")
41
+ version_parser.set_defaults(handler=_handle_version, format_hint="version")
38
42
  register_all_commands(subparsers)
39
43
  return parser
40
44
 
@@ -54,6 +58,8 @@ def run(
54
58
  err = stderr or sys.stderr
55
59
  parser = build_parser()
56
60
  normalized_argv = _normalize_global_args(list(argv) if argv is not None else sys.argv[1:])
61
+ if "--version" in normalized_argv:
62
+ return _emit_version(json_mode=_should_force_json_output_argv_for_version(normalized_argv), stdout=out)
57
63
  try:
58
64
  args = parser.parse_args(normalized_argv)
59
65
  except _CliArgumentError as exc:
@@ -76,6 +82,8 @@ def run(
76
82
  setattr(args, "_stdin", sys.stdin)
77
83
  setattr(args, "_stdout_stream", out)
78
84
  setattr(args, "_stderr_stream", err)
85
+ if getattr(args, "command", "") == "version":
86
+ return _emit_version(json_mode=bool(args.json), stdout=out)
79
87
  handler = getattr(args, "handler", None)
80
88
  if handler is None:
81
89
  parser.print_help(out)
@@ -111,6 +119,37 @@ def run(
111
119
  return exit_code
112
120
 
113
121
 
122
+ def _handle_version(_args: argparse.Namespace, _context: CliContext) -> dict[str, Any]:
123
+ version = get_cli_version()
124
+ return {
125
+ "ok": True,
126
+ "status": "success",
127
+ "version": version,
128
+ "package": "@qingflow-tech/qingflow-cli",
129
+ }
130
+
131
+
132
+ def _emit_version(*, json_mode: bool, stdout: TextIO) -> int:
133
+ version = get_cli_version()
134
+ if json_mode:
135
+ emit_json_result(
136
+ {
137
+ "ok": True,
138
+ "status": "success",
139
+ "version": version,
140
+ "package": "@qingflow-tech/qingflow-cli",
141
+ },
142
+ stream=stdout,
143
+ )
144
+ else:
145
+ stdout.write(f"{version}\n")
146
+ return 0
147
+
148
+
149
+ def _should_force_json_output_argv_for_version(argv: list[str]) -> bool:
150
+ return "--json" in argv
151
+
152
+
114
153
  def _normalize_global_args(argv: list[str]) -> list[str]:
115
154
  global_args: list[str] = []
116
155
  remaining: list[str] = []
@@ -121,6 +160,10 @@ def _normalize_global_args(argv: list[str]) -> list[str]:
121
160
  global_args.append(token)
122
161
  index += 1
123
162
  continue
163
+ if token == "--version":
164
+ global_args.append(token)
165
+ index += 1
166
+ continue
124
167
  if token == "--profile":
125
168
  global_args.append(token)
126
169
  if index + 1 >= len(argv):
@@ -146,8 +146,11 @@ class AiBuilderTools(ToolBase):
146
146
  package_id: int | None = None,
147
147
  package_name: str | None = None,
148
148
  create_if_missing: bool = False,
149
- icon: str | None = None,
149
+ icon: str | JSONObject | None = None,
150
150
  color: str | None = None,
151
+ icon_name: str | None = None,
152
+ icon_color: str | None = None,
153
+ icon_config: JSONObject | None = None,
151
154
  visibility: JSONObject | None = None,
152
155
  items: list[dict] | None = None,
153
156
  allow_detach: bool = False,
@@ -159,6 +162,9 @@ class AiBuilderTools(ToolBase):
159
162
  create_if_missing=create_if_missing,
160
163
  icon=icon,
161
164
  color=color,
165
+ icon_name=icon_name,
166
+ icon_color=icon_color,
167
+ icon_config=icon_config,
162
168
  visibility=visibility,
163
169
  items=items or None,
164
170
  allow_detach=allow_detach,
@@ -364,8 +370,11 @@ class AiBuilderTools(ToolBase):
364
370
  package_id: int | None = None,
365
371
  app_name: str = "",
366
372
  app_title: str = "",
367
- icon: str = "",
373
+ icon: str | JSONObject = "",
368
374
  color: str = "",
375
+ icon_name: str | None = None,
376
+ icon_color: str | None = None,
377
+ icon_config: JSONObject | None = None,
369
378
  visibility: JSONObject | None = None,
370
379
  create_if_missing: bool = False,
371
380
  publish: bool = True,
@@ -423,6 +432,9 @@ class AiBuilderTools(ToolBase):
423
432
  app_title=app_title,
424
433
  icon=icon,
425
434
  color=color,
435
+ icon_name=icon_name,
436
+ icon_color=icon_color,
437
+ icon_config=icon_config,
426
438
  visibility=visibility,
427
439
  create_if_missing=create_if_missing,
428
440
  publish=publish,
@@ -733,13 +745,23 @@ class AiBuilderTools(ToolBase):
733
745
  package_id: int | None = None,
734
746
  package_name: str | None = None,
735
747
  create_if_missing: bool = False,
736
- icon: str | None = None,
748
+ icon: str | JSONObject | None = None,
737
749
  color: str | None = None,
750
+ icon_name: str | None = None,
751
+ icon_color: str | None = None,
752
+ icon_config: JSONObject | None = None,
738
753
  visibility: JSONObject | None = None,
739
754
  items: list[dict] | None = None,
740
755
  allow_detach: bool = False,
741
756
  ) -> JSONObject:
742
757
  """执行分组与包相关逻辑。"""
758
+ icon, color = _normalize_builder_icon_args(
759
+ icon=icon,
760
+ color=color,
761
+ icon_name=icon_name,
762
+ icon_color=icon_color,
763
+ icon_config=icon_config,
764
+ )
743
765
  visibility_patch = None
744
766
  if visibility is not None:
745
767
  try:
@@ -1650,8 +1672,11 @@ class AiBuilderTools(ToolBase):
1650
1672
  package_id: int | None = None,
1651
1673
  app_name: str = "",
1652
1674
  app_title: str = "",
1653
- icon: str = "",
1675
+ icon: str | JSONObject = "",
1654
1676
  color: str = "",
1677
+ icon_name: str | None = None,
1678
+ icon_color: str | None = None,
1679
+ icon_config: JSONObject | None = None,
1655
1680
  visibility: JSONObject | None = None,
1656
1681
  create_if_missing: bool = False,
1657
1682
  publish: bool = True,
@@ -1661,7 +1686,15 @@ class AiBuilderTools(ToolBase):
1661
1686
  apps: list[JSONObject] | None = None,
1662
1687
  ) -> JSONObject:
1663
1688
  """执行应用相关逻辑。"""
1689
+ icon, color = _normalize_builder_icon_args(
1690
+ icon=icon,
1691
+ color=color,
1692
+ icon_name=icon_name,
1693
+ icon_color=icon_color,
1694
+ icon_config=icon_config,
1695
+ )
1664
1696
  if apps:
1697
+ apps = [_normalize_schema_app_item(item) if isinstance(item, dict) else item for item in apps]
1665
1698
  result = self._app_schema_apply_multi(
1666
1699
  profile=profile,
1667
1700
  package_id=package_id,
@@ -1801,6 +1834,7 @@ class AiBuilderTools(ToolBase):
1801
1834
  )
1802
1835
 
1803
1836
  client_key_to_app_key: dict[str, str] = {}
1837
+ app_name_to_app_key: dict[str, str] = {}
1804
1838
  created_app_keys: list[str] = []
1805
1839
  results: list[JSONObject] = []
1806
1840
  any_write_executed = False
@@ -1864,11 +1898,14 @@ class AiBuilderTools(ToolBase):
1864
1898
  created_app_keys.append(resolved_key)
1865
1899
  if client_key:
1866
1900
  client_key_to_app_key[client_key] = resolved_key
1901
+ resolved_name = str(public_shell.get("app_name_after") or public_shell.get("app_name") or app_name or "").strip()
1902
+ if resolved_name:
1903
+ app_name_to_app_key[resolved_name] = resolved_key
1867
1904
  results.append({
1868
1905
  "index": index,
1869
1906
  "row_number": index + 1,
1870
1907
  "client_key": client_key or None,
1871
- "app_name": str(public_shell.get("app_name_after") or public_shell.get("app_name") or app_name or "").strip() or None,
1908
+ "app_name": resolved_name or None,
1872
1909
  "app_key": resolved_key,
1873
1910
  "status": "shell_ready",
1874
1911
  "created": bool(public_shell.get("created")),
@@ -1888,7 +1925,7 @@ class AiBuilderTools(ToolBase):
1888
1925
  item = deepcopy(raw_item)
1889
1926
  app_key = str(existing.get("app_key") or "").strip()
1890
1927
  try:
1891
- compiled_item = _compile_multi_app_schema_item_refs(item, client_key_to_app_key)
1928
+ compiled_item = _compile_multi_app_schema_item_refs(item, client_key_to_app_key, app_name_to_app_key)
1892
1929
  except ValueError as error:
1893
1930
  final_items.append({
1894
1931
  **{key: existing.get(key) for key in ("index", "row_number", "client_key", "app_name", "app_key", "created")},
@@ -2786,8 +2823,58 @@ def _multi_app_item_failure(index: int, item: object, error_code: str, message:
2786
2823
  }
2787
2824
 
2788
2825
 
2789
- def _compile_multi_app_schema_item_refs(item: JSONObject, client_key_to_app_key: dict[str, str]) -> JSONObject:
2826
+ def _normalize_builder_icon_args(
2827
+ *,
2828
+ icon: str | JSONObject | None,
2829
+ color: str | None,
2830
+ icon_name: str | None = None,
2831
+ icon_color: str | None = None,
2832
+ icon_config: JSONObject | None = None,
2833
+ ) -> tuple[str | None, str | None]:
2834
+ payloads: list[JSONObject] = []
2835
+ if isinstance(icon_config, dict):
2836
+ payloads.append(icon_config)
2837
+ if isinstance(icon, dict):
2838
+ payloads.append(icon)
2839
+
2840
+ normalized_icon = None if isinstance(icon, dict) else icon
2841
+ normalized_color = color
2842
+ for payload in payloads:
2843
+ normalized_icon = normalized_icon or payload.get("icon") or payload.get("icon_name") or payload.get("iconName") or payload.get("name")
2844
+ normalized_color = normalized_color or payload.get("color") or payload.get("icon_color") or payload.get("iconColor")
2845
+ normalized_icon = normalized_icon or icon_name
2846
+ normalized_color = normalized_color or icon_color
2847
+ return (
2848
+ str(normalized_icon).strip() if normalized_icon is not None else None,
2849
+ str(normalized_color).strip() if normalized_color is not None else None,
2850
+ )
2851
+
2852
+
2853
+ def _normalize_schema_app_item(item: JSONObject) -> JSONObject:
2854
+ normalized = deepcopy(item)
2855
+ icon, color = _normalize_builder_icon_args(
2856
+ icon=normalized.get("icon"),
2857
+ color=normalized.get("color"),
2858
+ icon_name=normalized.get("icon_name") or normalized.get("iconName"),
2859
+ icon_color=normalized.get("icon_color") or normalized.get("iconColor"),
2860
+ icon_config=normalized.get("icon_config") or normalized.get("iconConfig"),
2861
+ )
2862
+ if icon:
2863
+ normalized["icon"] = icon
2864
+ if color:
2865
+ normalized["color"] = color
2866
+ for key in ("icon_name", "iconName", "icon_color", "iconColor", "icon_config", "iconConfig"):
2867
+ normalized.pop(key, None)
2868
+ return normalized
2869
+
2870
+
2871
+ def _compile_multi_app_schema_item_refs(
2872
+ item: JSONObject,
2873
+ client_key_to_app_key: dict[str, str],
2874
+ app_name_to_app_key: dict[str, str] | None = None,
2875
+ ) -> JSONObject:
2790
2876
  compiled = deepcopy(item)
2877
+ app_name_to_app_key = app_name_to_app_key or {}
2791
2878
 
2792
2879
  def visit(value):
2793
2880
  if isinstance(value, list):
@@ -2807,6 +2894,13 @@ def _compile_multi_app_schema_item_refs(item: JSONObject, client_key_to_app_key:
2807
2894
  if not target_app_key:
2808
2895
  raise ValueError(f"target_app_ref '{ref_key}' did not match any apps[].client_key")
2809
2896
  payload["target_app_key"] = target_app_key
2897
+ target_app = payload.pop("target_app", None) or payload.pop("targetApp", None)
2898
+ if target_app is not None and not str(payload.get("target_app_key") or "").strip():
2899
+ target_name = str(target_app or "").strip()
2900
+ target_app_key = app_name_to_app_key.get(target_name)
2901
+ if not target_app_key:
2902
+ raise ValueError(f"target_app '{target_name}' did not match any apps[].app_name in the same call")
2903
+ payload["target_app_key"] = target_app_key
2810
2904
  return payload
2811
2905
 
2812
2906
  return visit(compiled)
@@ -2855,7 +2949,7 @@ def _contains_multi_app_target_ref(value: object) -> bool:
2855
2949
  if not isinstance(value, dict):
2856
2950
  return False
2857
2951
  for key, entry in value.items():
2858
- if key in {"target_app_ref", "targetAppRef", "target_app_client_key", "targetAppClientKey"}:
2952
+ if key in {"target_app_ref", "targetAppRef", "target_app_client_key", "targetAppClientKey", "target_app", "targetApp"}:
2859
2953
  return True
2860
2954
  if _contains_multi_app_target_ref(entry):
2861
2955
  return True
@@ -4054,13 +4148,29 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
4054
4148
  },
4055
4149
  },
4056
4150
  "package_apply": {
4057
- "allowed_keys": ["package_id", "package_name", "create_if_missing", "icon", "color", "visibility", "items", "allow_detach"],
4151
+ "allowed_keys": [
4152
+ "package_id",
4153
+ "package_name",
4154
+ "create_if_missing",
4155
+ "icon",
4156
+ "color",
4157
+ "icon_name",
4158
+ "icon_color",
4159
+ "icon_config",
4160
+ "visibility",
4161
+ "items",
4162
+ "allow_detach",
4163
+ ],
4058
4164
  "aliases": {
4059
4165
  "packageId": "package_id",
4060
4166
  "packageName": "package_name",
4061
4167
  "createIfMissing": "create_if_missing",
4062
4168
  "iconName": "icon",
4063
4169
  "iconColor": "color",
4170
+ "icon_config.name": "icon",
4171
+ "icon_config.icon_name": "icon",
4172
+ "icon_config.color": "color",
4173
+ "icon_config.icon_color": "color",
4064
4174
  "allowDetach": "allow_detach",
4065
4175
  },
4066
4176
  "allowed_values": {
@@ -4072,6 +4182,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
4072
4182
  "execution_notes": [
4073
4183
  "create or update package metadata, visibility, grouping, and ordering in one call",
4074
4184
  "creating a package requires explicit icon + color; icon=template is blocked because it is too generic",
4185
+ "icon can be passed as icon/color, icon_name/icon_color, icon_config, or icon={name,color}; all forms normalize to icon/color",
4075
4186
  "updating a package preserves existing icon/color when omitted; explicit icon/color values are still validated",
4076
4187
  "call workspace_icon_catalog_get or `qingflow --json builder icon catalog` for supported icon/color candidates",
4077
4188
  "metadata keys omitted on update are preserved",
@@ -4575,6 +4686,9 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
4575
4686
  "app_title",
4576
4687
  "icon",
4577
4688
  "color",
4689
+ "icon_name",
4690
+ "icon_color",
4691
+ "icon_config",
4578
4692
  "visibility",
4579
4693
  "create_if_missing",
4580
4694
  "publish",
@@ -4587,11 +4701,15 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
4587
4701
  "apps[].app_name",
4588
4702
  "apps[].icon",
4589
4703
  "apps[].color",
4704
+ "apps[].icon_name",
4705
+ "apps[].icon_color",
4706
+ "apps[].icon_config",
4590
4707
  "apps[].visibility",
4591
4708
  "apps[].add_fields",
4592
4709
  "apps[].update_fields",
4593
4710
  "apps[].remove_fields",
4594
4711
  "apps[].add_fields[].target_app_ref",
4712
+ "apps[].add_fields[].target_app",
4595
4713
  ],
4596
4714
  "aliases": {
4597
4715
  "app_title": "app_name",
@@ -4601,8 +4719,16 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
4601
4719
  "apps[].appKey": "apps[].app_key",
4602
4720
  "apps[].appName": "apps[].app_name",
4603
4721
  "apps[].appTitle": "apps[].app_name",
4722
+ "apps[].iconName": "apps[].icon",
4723
+ "apps[].iconColor": "apps[].color",
4724
+ "apps[].icon_config.name": "apps[].icon",
4725
+ "apps[].icon_config.color": "apps[].color",
4604
4726
  "field.targetAppRef": "field.target_app_ref",
4605
4727
  "field.targetAppClientKey": "field.target_app_ref",
4728
+ "field.targetApp": "field.target_app",
4729
+ "field.options[].label": "field.options[]",
4730
+ "field.options[].value": "field.options[]",
4731
+ "field.options[].optValue": "field.options[]",
4606
4732
  "field.title": "field.name",
4607
4733
  "field.label": "field.name",
4608
4734
  "field.fields": "field.subfields",
@@ -4642,10 +4768,13 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
4642
4768
  "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",
4643
4769
  "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",
4644
4770
  "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",
4771
+ "multi-app relation fields may also use target_app with another apps[].app_name; prefer target_app_ref/client_key when names may collide",
4645
4772
  "multi-app mode is not transactional; read created_app_keys and apps[].status before retrying, and retry only failed app items",
4646
4773
  "create mode defaults new app visibility to workspace/not when visibility is omitted; edit mode preserves current visibility when omitted",
4647
4774
  "create mode requires explicit icon + color; icon=template is blocked because it is too generic",
4775
+ "icon can be passed as icon/color, icon_name/icon_color, icon_config, or icon={name,color}; all forms normalize to icon/color",
4648
4776
  "multi-app create mode requires each new app item to include a distinct non-template icon and a valid color",
4777
+ "single_select and multi_select options accept strings or objects such as {label,value}; builder normalizes them to option labels before writing",
4649
4778
  "edit mode preserves existing icon/color when omitted; explicit icon/color values are still validated",
4650
4779
  "call workspace_icon_catalog_get or `qingflow --json builder icon catalog` for supported icon/color candidates",
4651
4780
  *_VISIBILITY_EXECUTION_NOTES,
@@ -0,0 +1,40 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from importlib import metadata
5
+ from pathlib import Path
6
+
7
+
8
+ def get_cli_version() -> str:
9
+ package_json_version = _find_package_json_version()
10
+ if package_json_version:
11
+ return package_json_version
12
+ try:
13
+ return metadata.version("qingflow-mcp")
14
+ except metadata.PackageNotFoundError:
15
+ return "0+local"
16
+
17
+
18
+ def _find_package_json_version() -> str | None:
19
+ current = Path(__file__).resolve()
20
+ for parent in current.parents:
21
+ package_json = parent / "package.json"
22
+ if not package_json.exists():
23
+ continue
24
+ try:
25
+ payload = json.loads(package_json.read_text(encoding="utf-8"))
26
+ except (OSError, json.JSONDecodeError):
27
+ continue
28
+ name = str(payload.get("name") or "")
29
+ version = str(payload.get("version") or "")
30
+ if version and name in {
31
+ "qingflow-mcp-workspace",
32
+ "@qingflow-tech/qingflow-cli",
33
+ "@qingflow-tech/qingflow-app-user-mcp",
34
+ "@qingflow-tech/qingflow-app-builder-mcp",
35
+ "@josephyan/qingflow-cli",
36
+ "@josephyan/qingflow-app-user-mcp",
37
+ "@josephyan/qingflow-app-builder-mcp",
38
+ }:
39
+ return version
40
+ return None