@qingflow-tech/qingflow-app-user-mcp 1.0.10 → 1.0.11

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.
@@ -13,30 +13,17 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
13
13
  app_subparsers = parser.add_subparsers(dest="app_command", required=True)
14
14
 
15
15
  list_parser = app_subparsers.add_parser("list", help="列出可见应用")
16
+ list_parser.add_argument("--query", default="", help="按关键词在可见应用列表中本地过滤")
17
+ list_parser.add_argument("--keyword", default="", help="兼容别名;建议使用 --query")
16
18
  list_parser.set_defaults(handler=_handle_list, format_hint="app_list")
17
19
 
18
- search = app_subparsers.add_parser("search", help="搜索应用")
19
- search.add_argument("--keyword", default="")
20
- search.add_argument("--page", type=int, default=1)
21
- search.add_argument("--page-size", type=int, default=50)
22
- search.set_defaults(handler=_handle_search, format_hint="app_search")
23
-
24
20
  get = app_subparsers.add_parser("get", help="读取应用可访问视图与导入能力")
25
21
  get.add_argument("--app-key", help="不传时在交互终端中选择应用")
26
22
  get.set_defaults(handler=_handle_get, format_hint="app_get")
27
23
 
28
24
 
29
25
  def _handle_list(args: argparse.Namespace, context: CliContext) -> dict:
30
- return context.app.app_list(profile=args.profile)
31
-
32
-
33
- def _handle_search(args: argparse.Namespace, context: CliContext) -> dict:
34
- return context.app.app_search(
35
- profile=args.profile,
36
- keyword=args.keyword,
37
- page_num=args.page,
38
- page_size=args.page_size,
39
- )
26
+ return context.app.app_list(profile=args.profile, query=args.query, keyword=args.keyword)
40
27
 
41
28
 
42
29
  def _handle_get(args: argparse.Namespace, context: CliContext) -> dict:
@@ -42,6 +42,11 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
42
42
  contract.add_argument("--tool-name", required=True)
43
43
  contract.set_defaults(handler=_handle_contract, format_hint="builder_summary")
44
44
 
45
+ icon = builder_subparsers.add_parser("icon", help="工作区图标目录")
46
+ icon_subparsers = icon.add_subparsers(dest="builder_icon_command", required=True)
47
+ icon_catalog = icon_subparsers.add_parser("catalog", help="读取应用、应用包、门户可用图标目录")
48
+ icon_catalog.set_defaults(handler=_handle_icon_catalog, format_hint="builder_summary")
49
+
45
50
  member = builder_subparsers.add_parser("member", help="成员目录")
46
51
  member_subparsers = member.add_subparsers(dest="builder_member_command", required=True)
47
52
  member_search = member_subparsers.add_parser("search", help="搜索成员")
@@ -78,6 +83,11 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
78
83
  solution_install.add_argument("--solution-source", default="solutionDetail")
79
84
  solution_install.set_defaults(handler=_handle_solution_install, format_hint="builder_summary")
80
85
 
86
+ package_list = package_subparsers.add_parser("list", help="列出应用包")
87
+ package_list.add_argument("--trial-status", default="all")
88
+ package_list.add_argument("--query", default="")
89
+ package_list.set_defaults(handler=_handle_package_list, format_hint="builder_summary")
90
+
81
91
  package_get = package_subparsers.add_parser("get", help="读取应用包详情")
82
92
  package_get.add_argument("--package-id", type=int, required=True)
83
93
  package_get.set_defaults(handler=_handle_package_get, format_hint="builder_summary")
@@ -157,7 +167,9 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
157
167
  portal_apply.add_argument("--dash-name", default="")
158
168
  portal_apply.add_argument("--package-id", type=int)
159
169
  portal_apply.add_argument("--publish", action=argparse.BooleanOptionalAction, default=True)
170
+ portal_apply.add_argument("--payload-file")
160
171
  portal_apply.add_argument("--sections-file")
172
+ portal_apply.add_argument("--layout-preset", default="")
161
173
  portal_apply.add_argument("--visibility-file")
162
174
  portal_apply.add_argument("--auth-file")
163
175
  portal_apply.add_argument("--icon")
@@ -277,6 +289,10 @@ def _handle_contract(args: argparse.Namespace, context: CliContext) -> dict:
277
289
  return context.builder.builder_tool_contract(tool_name=args.tool_name)
278
290
 
279
291
 
292
+ def _handle_icon_catalog(args: argparse.Namespace, context: CliContext) -> dict:
293
+ return context.builder.workspace_icon_catalog_get(profile=args.profile)
294
+
295
+
280
296
  def _handle_member_search(args: argparse.Namespace, context: CliContext) -> dict:
281
297
  return context.builder.member_search(
282
298
  profile=args.profile,
@@ -320,6 +336,10 @@ def _handle_package_get(args: argparse.Namespace, context: CliContext) -> dict:
320
336
  return context.builder.package_get(profile=args.profile, package_id=args.package_id)
321
337
 
322
338
 
339
+ def _handle_package_list(args: argparse.Namespace, context: CliContext) -> dict:
340
+ return context.builder.package_list(profile=args.profile, trial_status=args.trial_status, query=args.query)
341
+
342
+
323
343
  def _handle_package_apply(args: argparse.Namespace, context: CliContext) -> dict:
324
344
  config = load_object_arg(args.config_file, option_name="--config-file")
325
345
  if not isinstance(config, dict):
@@ -557,9 +577,13 @@ def _handle_charts_apply(args: argparse.Namespace, context: CliContext) -> dict:
557
577
 
558
578
 
559
579
  def _handle_portal_apply(args: argparse.Namespace, context: CliContext) -> dict:
580
+ payload = load_object_arg(args.payload_file, option_name="--payload-file") if getattr(args, "payload_file", None) else None
581
+ payload_obj = payload if isinstance(payload, dict) else {}
582
+ effective_dash_name = (args.dash_name or str(payload_obj.get("dash_name") or payload_obj.get("dashName") or payload_obj.get("name") or "")).strip()
583
+ effective_package_id = args.package_id if args.package_id is not None else payload_obj.get("package_id") or payload_obj.get("packageId") or payload_obj.get("package_tag_id")
560
584
  has_dash_key = bool((args.dash_key or "").strip())
561
- has_dash_name = bool((args.dash_name or "").strip())
562
- has_package_id = args.package_id is not None
585
+ has_dash_name = bool(effective_dash_name)
586
+ has_package_id = effective_package_id is not None
563
587
  if has_dash_key and has_package_id:
564
588
  raise_config_error(
565
589
  "portal apply accepts exactly one selector mode.",
@@ -578,6 +602,7 @@ def _handle_portal_apply(args: argparse.Namespace, context: CliContext) -> dict:
578
602
  package_id=args.package_id,
579
603
  publish=bool(args.publish),
580
604
  sections=sections,
605
+ layout_preset=args.layout_preset,
581
606
  visibility=load_object_arg(args.visibility_file, option_name="--visibility-file"),
582
607
  auth=load_object_arg(args.auth_file, option_name="--auth-file"),
583
608
  icon=args.icon,
@@ -585,6 +610,7 @@ def _handle_portal_apply(args: argparse.Namespace, context: CliContext) -> dict:
585
610
  hide_copyright=args.hide_copyright,
586
611
  dash_global_config=load_object_arg(args.dash_global_config_file, option_name="--dash-global-config-file"),
587
612
  config=load_object_arg(args.config_file, option_name="--config-file"),
613
+ payload=payload,
588
614
  )
589
615
 
590
616
 
@@ -13,7 +13,7 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
13
13
  record_subparsers = parser.add_subparsers(
14
14
  dest="record_command",
15
15
  required=True,
16
- metavar="{schema,list,access,get,insert,update,delete,member-candidates,department-candidates,code-block-run}",
16
+ metavar="{schema,list,access,get,logs,insert,update,delete,member-candidates,department-candidates,code-block-run}",
17
17
  )
18
18
 
19
19
  schema = record_subparsers.add_parser("schema", help="读取记录相关表结构")
@@ -102,6 +102,12 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
102
102
  get.add_argument("--view-id")
103
103
  get.set_defaults(handler=_handle_get, format_hint="record_get")
104
104
 
105
+ logs = record_subparsers.add_parser("logs", help="读取单条记录全量日志并写入本地 JSONL")
106
+ logs.add_argument("--app-key", required=True)
107
+ logs.add_argument("--record-id", required=True)
108
+ logs.add_argument("--view-id")
109
+ logs.set_defaults(handler=_handle_logs, format_hint="record_logs")
110
+
105
111
  insert = record_subparsers.add_parser("insert", help="新增记录")
106
112
  insert.add_argument("--app-key", required=True)
107
113
  insert.add_argument("--fields-file", help=argparse.SUPPRESS)
@@ -328,6 +334,15 @@ def _handle_get(args: argparse.Namespace, context: CliContext) -> dict:
328
334
  )
329
335
 
330
336
 
337
+ def _handle_logs(args: argparse.Namespace, context: CliContext) -> dict:
338
+ return context.record.record_logs_get(
339
+ profile=args.profile,
340
+ app_key=args.app_key,
341
+ record_id=args.record_id,
342
+ view_id=args.view_id,
343
+ )
344
+
345
+
331
346
  def _handle_insert(args: argparse.Namespace, context: CliContext) -> dict:
332
347
  if args.items_file:
333
348
  if args.fields_file:
@@ -292,6 +292,37 @@ def _format_record_get(result: dict[str, Any]) -> str:
292
292
  return "\n".join(lines) + "\n"
293
293
 
294
294
 
295
+ def _format_record_logs(result: dict[str, Any]) -> str:
296
+ app = result.get("app") if isinstance(result.get("app"), dict) else {}
297
+ view = result.get("view") if isinstance(result.get("view"), dict) else {}
298
+ record = result.get("record") if isinstance(result.get("record"), dict) else {}
299
+ data_logs = result.get("data_logs") if isinstance(result.get("data_logs"), dict) else {}
300
+ workflow_logs = result.get("workflow_logs") if isinstance(result.get("workflow_logs"), dict) else {}
301
+ integrity = result.get("context_integrity") if isinstance(result.get("context_integrity"), dict) else {}
302
+ lines = [
303
+ f"Status: {result.get('status') or '-'}",
304
+ f"App: {app.get('app_name') or app.get('app_key') or '-'} ({app.get('app_key') or '-'})",
305
+ f"View: {view.get('name') or view.get('view_id') or '-'}",
306
+ f"Record: {record.get('title') or '-'} ({record.get('record_id') or '-'})",
307
+ f"Data logs: {data_logs.get('status') or '-'} / count={data_logs.get('items_count')} / pages={data_logs.get('pages_fetched')} / complete={data_logs.get('complete')}",
308
+ f"Workflow logs: {workflow_logs.get('status') or '-'} / count={workflow_logs.get('items_count')} / pages={workflow_logs.get('pages_fetched')} / complete={workflow_logs.get('complete')}",
309
+ f"Safe for full log conclusion: {integrity.get('safe_for_full_log_conclusion')}",
310
+ ]
311
+ if result.get("local_dir"):
312
+ lines.append(f"Local dir: {result.get('local_dir')}")
313
+ if data_logs.get("local_path"):
314
+ lines.append(f"Data logs file: {data_logs.get('local_path')}")
315
+ if workflow_logs.get("local_path"):
316
+ lines.append(f"Workflow logs file: {workflow_logs.get('local_path')}")
317
+ if result.get("summary_path"):
318
+ lines.append(f"Summary file: {result.get('summary_path')}")
319
+ _append_warnings(lines, result.get("warnings"))
320
+ unavailable = result.get("unavailable_context") if isinstance(result.get("unavailable_context"), list) else []
321
+ if unavailable:
322
+ lines.append(f"Unavailable contexts: {len(unavailable)}")
323
+ return "\n".join(lines) + "\n"
324
+
325
+
295
326
  def _format_task_list(result: dict[str, Any]) -> str:
296
327
  data = result.get("data") if isinstance(result.get("data"), dict) else {}
297
328
  items = data.get("items") if isinstance(data.get("items"), list) else []
@@ -788,11 +819,11 @@ _FORMATTERS = {
788
819
  "workspace_get": _format_workspace_get,
789
820
  "workspace_select": _format_workspace_select,
790
821
  "app_list": _format_app_items,
791
- "app_search": _format_app_items,
792
822
  "app_get": _format_app_get,
793
823
  "record_list": _format_record_list,
794
824
  "record_access": _format_record_access,
795
825
  "record_get": _format_record_get,
826
+ "record_logs": _format_record_logs,
796
827
  "task_list": _format_task_list,
797
828
  "task_workbench": _format_task_workbench,
798
829
  "task_get": _format_task_get,
@@ -38,7 +38,6 @@ USER_PUBLIC_TOOL_SPECS: tuple[PublicToolSpec, ...] = (
38
38
  PublicToolSpec(USER_DOMAIN, "workspace_get", ("workspace_get",), ("workspace", "get")),
39
39
  PublicToolSpec(USER_DOMAIN, "workspace_select", ("workspace_select",), ("workspace", "select")),
40
40
  PublicToolSpec(USER_DOMAIN, "app_list", ("app_list",), ("app", "list"), cli_show_effective_context=True),
41
- PublicToolSpec(USER_DOMAIN, "app_search", ("app_search",), ("app", "search"), cli_show_effective_context=True),
42
41
  PublicToolSpec(USER_DOMAIN, "app_get", ("app_get",), ("app", "get"), cli_show_effective_context=True),
43
42
  PublicToolSpec(USER_DOMAIN, "portal_list", ("portal_list",), ("portal", "list"), cli_show_effective_context=True),
44
43
  PublicToolSpec(USER_DOMAIN, "portal_get", ("portal_get",), ("portal", "get"), cli_show_effective_context=True),
@@ -84,6 +83,7 @@ USER_PUBLIC_TOOL_SPECS: tuple[PublicToolSpec, ...] = (
84
83
  PublicToolSpec(USER_DOMAIN, "record_list", ("record_list",), ("record", "list"), cli_show_effective_context=True),
85
84
  PublicToolSpec(USER_DOMAIN, "record_access", ("record_access",), ("record", "access"), cli_show_effective_context=True),
86
85
  PublicToolSpec(USER_DOMAIN, "record_get", ("record_get_public",), ("record", "get"), cli_show_effective_context=True),
86
+ PublicToolSpec(USER_DOMAIN, "record_logs_get", ("record_logs_get",), ("record", "logs"), cli_show_effective_context=True),
87
87
  PublicToolSpec(USER_DOMAIN, "record_insert", ("record_insert_public",), ("record", "insert"), cli_show_effective_context=True, cli_context_write=True),
88
88
  PublicToolSpec(USER_DOMAIN, "record_update", ("record_update_public",), ("record", "update"), cli_show_effective_context=True, cli_context_write=True),
89
89
  PublicToolSpec(USER_DOMAIN, "record_delete", ("record_delete_public",), ("record", "delete"), cli_show_effective_context=True, cli_context_write=True),
@@ -127,6 +127,8 @@ BUILDER_PUBLIC_TOOL_SPECS: tuple[PublicToolSpec, ...] = (
127
127
  PublicToolSpec(BUILDER_DOMAIN, "file_upload_local", ("file_upload_local",), ("builder", "file", "upload-local"), has_contract=True, cli_show_effective_context=True, cli_context_write=True),
128
128
  PublicToolSpec(BUILDER_DOMAIN, "feedback_submit", ("feedback_submit",), ("builder", "feedback", "submit"), has_contract=True),
129
129
  PublicToolSpec(BUILDER_DOMAIN, "builder_tool_contract", ("builder_tool_contract",), ("builder", "contract"), has_contract=False),
130
+ PublicToolSpec(BUILDER_DOMAIN, "workspace_icon_catalog_get", ("workspace_icon_catalog_get",), ("builder", "icon", "catalog"), has_contract=True, cli_show_effective_context=True),
131
+ PublicToolSpec(BUILDER_DOMAIN, "package_list", ("package_list",), ("builder", "package", "list"), has_contract=True, cli_show_effective_context=True),
130
132
  PublicToolSpec(BUILDER_DOMAIN, "package_get", ("package_get",), ("builder", "package", "get"), has_contract=True, cli_show_effective_context=True),
131
133
  PublicToolSpec(BUILDER_DOMAIN, "package_apply", ("package_apply",), ("builder", "package", "apply"), has_contract=True, cli_show_effective_context=True, cli_context_write=True),
132
134
  PublicToolSpec(BUILDER_DOMAIN, "solution_install", ("solution_install",), ("builder", "solution", "install"), has_contract=True, cli_show_effective_context=True, cli_context_write=True),
@@ -73,7 +73,7 @@ def trim_success_response(tool_name: str | None, payload: dict[str, Any]) -> dic
73
73
  return payload
74
74
  trimmed = deepcopy(payload)
75
75
  drop_keys = COMMON_SUCCESS_DROP_TOP
76
- if tool_name == "user:record_get":
76
+ if tool_name in {"user:record_get", "user:record_logs_get"}:
77
77
  drop_keys = COMMON_SUCCESS_DROP_TOP - {"output_profile"}
78
78
  if tool_name in {"user:record_insert", "user:record_update", "user:record_delete"} and payload.get("ok") is False:
79
79
  drop_keys = drop_keys - {"ok"}
@@ -294,7 +294,7 @@ def _trim_workspace_get(payload: JSONObject) -> None:
294
294
  )
295
295
 
296
296
 
297
- def _trim_app_search_like(payload: JSONObject) -> None:
297
+ def _trim_app_list_like(payload: JSONObject) -> None:
298
298
  payload.pop("apps", None)
299
299
  _trim_item_list(payload, "items", allowed=("app_key", "app_name", "package_name"))
300
300
 
@@ -394,6 +394,10 @@ def _trim_record_write(payload: JSONObject) -> None:
394
394
  data.pop("normalized_payload", None)
395
395
  data.pop("human_review", None)
396
396
  data.pop("action", None)
397
+ for key in ("update_route", "tried_routes"):
398
+ value = payload.get(key)
399
+ if value not in (None, [], {}, ""):
400
+ data[key] = value
397
401
  resource = _compact_record_resource(data.get("resource"))
398
402
  if resource:
399
403
  data["resource"] = resource
@@ -439,6 +443,8 @@ def _trim_record_write_batch(payload: JSONObject, data: JSONObject) -> None:
439
443
  "write_executed",
440
444
  "verification_status",
441
445
  "safe_to_retry",
446
+ "update_route",
447
+ "tried_routes",
442
448
  "failed_fields",
443
449
  "confirmation_requests",
444
450
  "blockers",
@@ -668,6 +674,49 @@ def _trim_record_access(payload: JSONObject) -> None:
668
674
  payload.update(compact)
669
675
 
670
676
 
677
+ def _trim_record_logs(payload: JSONObject) -> None:
678
+ compact: dict[str, Any] = {}
679
+ for key in (
680
+ "ok",
681
+ "status",
682
+ "output_profile",
683
+ "app",
684
+ "view",
685
+ "record",
686
+ "local_dir",
687
+ "summary_path",
688
+ "warnings",
689
+ "unavailable_context",
690
+ "context_integrity",
691
+ ):
692
+ value = payload.get(key)
693
+ if value is not None:
694
+ compact[key] = value
695
+ for key in ("data_logs", "workflow_logs"):
696
+ node = payload.get(key)
697
+ if isinstance(node, dict):
698
+ compact[key] = _pick(
699
+ node,
700
+ (
701
+ "status",
702
+ "visible",
703
+ "source",
704
+ "reason",
705
+ "complete",
706
+ "items_count",
707
+ "pages_fetched",
708
+ "page_size",
709
+ "reported_total",
710
+ "local_path",
711
+ "preview_items",
712
+ "warnings",
713
+ "stopped_reason",
714
+ ),
715
+ )
716
+ payload.clear()
717
+ payload.update(compact)
718
+
719
+
671
720
  def _trim_record_analyze(payload: JSONObject) -> None:
672
721
  summary: dict[str, Any] = {}
673
722
  completeness = payload.get("completeness")
@@ -984,7 +1033,7 @@ _register_policy((USER_DOMAIN, BUILDER_DOMAIN), ("auth_use_credential", "auth_wh
984
1033
  _register_policy((USER_DOMAIN, BUILDER_DOMAIN), ("auth_logout",), _trim_auth_logout)
985
1034
  _register_policy((USER_DOMAIN, BUILDER_DOMAIN), ("workspace_list",), _trim_workspace_list)
986
1035
  _register_policy((USER_DOMAIN, BUILDER_DOMAIN), ("workspace_get", "workspace_select"), _trim_workspace_get)
987
- _register_policy((USER_DOMAIN,), ("app_list", "app_search"), _trim_app_search_like)
1036
+ _register_policy((USER_DOMAIN,), ("app_list",), _trim_app_list_like)
988
1037
  _register_policy((USER_DOMAIN, BUILDER_DOMAIN), ("app_get",), _trim_app_get)
989
1038
  _register_policy((BUILDER_DOMAIN,), ("app_repair_code_blocks",), _trim_builder_list_like)
990
1039
  _register_policy((USER_DOMAIN, BUILDER_DOMAIN), ("portal_list", "portal_get", "view_get", "chart_get"), _trim_builder_list_like)
@@ -1027,6 +1076,7 @@ _register_policy((USER_DOMAIN,), ("record_insert", "record_update"), _trim_recor
1027
1076
  _register_policy((USER_DOMAIN,), ("record_get",), _trim_record_get)
1028
1077
  _register_policy((USER_DOMAIN,), ("record_list",), _trim_record_list)
1029
1078
  _register_policy((USER_DOMAIN,), ("record_access",), _trim_record_access)
1079
+ _register_policy((USER_DOMAIN,), ("record_logs_get",), _trim_record_logs)
1030
1080
  _register_policy((USER_DOMAIN,), ("record_analyze",), _trim_record_analyze)
1031
1081
  _register_policy((USER_DOMAIN,), ("record_code_block_run",), _trim_code_block_run)
1032
1082
  _register_policy((USER_DOMAIN,), ("task_list",), _trim_task_list)
@@ -1067,6 +1117,8 @@ _register_policy(
1067
1117
  (BUILDER_DOMAIN,),
1068
1118
  (
1069
1119
  "builder_tool_contract",
1120
+ "workspace_icon_catalog_get",
1121
+ "package_list",
1070
1122
  "package_get",
1071
1123
  "package_apply",
1072
1124
  "solution_install",
@@ -48,7 +48,7 @@ All resource tools operate with the logged-in user's Qingflow permissions.
48
48
 
49
49
  ## App Discovery
50
50
 
51
- If `app_key` is unknown, use `app_list` or `app_search` first.
51
+ If `app_key` is unknown, use `app_list` first. Pass `query` to filter visible apps by keyword.
52
52
  If the app is known but the data range is not, use `app_get` first and choose from `accessible_views`.
53
53
  If an accessible view has `analysis_supported=false`, do not use it for `record_access` or `record_list`. `boardView` and `ganttView` are special UI views, not data-access targets.
54
54
  `view_get(view_id=...)` also returns `export_capability`; it only means there is a supported export route, not that export permission has been verified.
@@ -56,9 +56,9 @@ If an accessible view has `analysis_supported=false`, do not use it for `record_
56
56
  ## Schema-First Rule
57
57
 
58
58
  Call `record_insert_schema_get` before `record_insert`.
59
- Call `record_update_schema_get` before `record_update`.
59
+ For simple field changes after the target record is clear, call `record_update` directly. Use `record_update_schema_get` for diagnostics, ambiguous fields, or complex writable-scope inspection.
60
60
  Call `record_code_block_schema_get` before `record_code_block_run`.
61
- Call `app_get` first when the data range is unclear, then use `record_browse_schema_get(view_id=...)` before `record_access`, `record_list`, or `record_get`.
61
+ Call `app_get` first when the data range is unclear, then use `record_browse_schema_get(view_id=...)` before `record_access`, `record_list`, `record_get`, or `record_logs_get`.
62
62
  Call `record_import_schema_get` when the import field mapping is unclear before template download or verify.
63
63
 
64
64
  - All `field_id` values must come from the schema response.
@@ -67,7 +67,7 @@ Call `record_import_schema_get` when the import field mapping is unclear before
67
67
  ## Schema Scope
68
68
 
69
69
  `record_insert_schema_get` returns the current user's insert-ready applicant schema; read `required_fields`, `optional_fields`, `runtime_linked_required_fields`, and `payload_template`.
70
- `record_update_schema_get` returns the current record's overall update-ready writable field set across matched accessible views; read `writable_fields` and `payload_template`.
70
+ `record_update_schema_get` returns the current record's overall update-ready writable field set and route diagnostics across matched accessible views; read `writable_fields`, `payload_template`, `available_update_routes`, and `recommended_update_route`.
71
71
  `record_browse_schema_get(view_id=...)` returns the same readable fields shown in the selected Qingflow table view header.
72
72
  `record_access.fields` / CSV columns and `record_list.columns / where / order_by / query_fields` use that exact same view schema.
73
73
  `record_code_block_schema_get` returns code-block-ready schema for exact code block field selection.
@@ -103,9 +103,9 @@ Analysis answers must include concrete numbers. When applicable, include percent
103
103
 
104
104
  ## Record CRUD Path
105
105
 
106
- `app_get -> record_browse_schema_get(view_id=...) -> record_list / record_get`
106
+ `app_get -> record_browse_schema_get(view_id=...) -> record_list / record_get / record_logs_get`
107
107
  `record_insert_schema_get -> record_insert(items)`
108
- `record_update_schema_get -> record_update`
108
+ `record_update` for simple updates; `record_update_schema_get -> record_update` when the writable field scope is unclear.
109
109
  `record_list / record_get -> record_delete`
110
110
  `record_code_block_schema_get -> record_code_block_run`
111
111
 
@@ -116,15 +116,16 @@ Analysis answers must include concrete numbers. When applicable, include percent
116
116
  - Legacy forms such as bare integer `field_id`, `fieldId`, `operator`, `values`, or `order` may still parse, but they are compatibility-only and not the canonical DSL
117
117
 
118
118
  - `record_insert` defaults to an applicant-node `items` array; each item contains a field-title keyed `fields` map. A single insert is one item.
119
- - `record_update` uses a field-title keyed `fields` map and internally selects the first accessible view that can execute the current payload.
119
+ - `record_update` uses a field-title keyed `fields` map. It first tries the data-manager direct update route, then falls back to the frontend custom-view detail edit route when the selected view can cover the payload; if a unique current-user todo task for the same record exposes editable fields, it can finally use the workflow save-only route. Read `update_route` and `tried_routes` after execution.
120
120
  - For insert, `runtime_linked_required_fields` means required-but-not-directly-writable fields that are usually supplied by runtime linkage or upstream context.
121
121
  - For insert, fields marked `may_become_required=true` stay in `optional_fields`; they are still directly writable, but linked visibility or option-driven rules can make them required at runtime.
122
122
  - Read field-level `linkage` whenever present on `record_insert_schema_get` or `record_update_schema_get`; it is the static hint for linked visibility, reference-driven auto fill, and formula/default auto-fill behavior.
123
123
  - `linkage.sources` lists upstream field titles that influence the current field; `linkage.affects_fields` lists downstream fields that may change when the current field changes.
124
124
  - `linkage.kind=logic_visibility` means linked visibility or option-driven rules are involved; `linkage.kind=reference_fill` means reference/default matching logic is involved; `linkage.kind=formula_fill` means formula/default auto-fill logic is involved.
125
- - `record_update_schema_get` exposes the overall writable field set for the record, but not every field combination is guaranteed; `record_update` still needs one single matched accessible view that can cover the payload.
125
+ - `record_update_schema_get` exposes the overall writable field set and route candidates for the record, but not every field combination is guaranteed; `record_update` still needs data-manager permission, one single matched custom view that can cover the payload, or one unique editable current-user todo task.
126
126
  - `record_delete` deletes by `record_id` or `record_ids`.
127
127
  - `record_get` is the single-record frontend detail context tool. It returns detail-page visible fields, one-level relation targets, first-page data/workflow logs, associated views/reports, local readable image assets, local downloadable file assets, unavailable context, and `semantic_context`.
128
+ - Use `record_logs_get` only when the user needs the full visible data/workflow log history for a specific record. It writes JSONL files locally and returns file paths plus completeness metadata; do not expect full log arrays in the response.
128
129
  - Read record images from `record_get.media_assets.items[].local_path` when `readable_by_agent=true`; read attachments/documents/tables from `record_get.file_assets.items[].local_path` and `extraction.text_path` when present. `record_get` follows the frontend storage cookie redirect path for Qingflow attachments, and remote file URLs should not be treated as directly readable.
129
130
  - `record_get.columns` are focus hints only; they do not project the detail fields. Read facts from top-level `fields[]`.
130
131
  - When readback shape matters after insert or update, prefer `record_get` for human/detail-page context, or `record_list(..., output_profile="normalized")` for batch-shaped normalized rows.
@@ -185,7 +186,7 @@ Use `record_code_block_run` when the user wants to execute a form code-block fie
185
186
  - `task_action_execute(task_id=..., action=...)` is also supported; MCP resolves the current todo locator internally before calling the real action route.
186
187
  - `task_workflow_log_get(task_id=...)` and `task_associated_report_detail_get(task_id=...)` are also supported for the current todo context.
187
188
  - Use `task_associated_report_detail_get` for associated view or report details.
188
- - Use `task_workflow_log_get` for full workflow log history.
189
+ - Use `task_workflow_log_get` for the current task context workflow log page. For full record-level data/workflow logs, use `record_logs_get(app_key, record_id, view_id?)`.
189
190
  - Task actions operate on `app_key + record_id + workflow_node_id`, not `task_id`.
190
191
 
191
192
  ## Time Handling
@@ -35,16 +35,18 @@ def build_builder_server() -> FastMCP:
35
35
  "Follow the resource path resolve -> summary read -> apply -> publish_verify. "
36
36
  "Use builder_tool_contract when you need a machine-readable contract, aliases, allowed enums, or a minimal valid example for a public builder tool. "
37
37
  "Use solution_install when the user explicitly wants to install a packaged solution/template by solution_key, optionally copying bundled demo data. "
38
- "If creating or updating an app package may be appropriate, use package_apply with explicit user intent; otherwise use package_get and app_resolve to locate resources, "
38
+ "Use package_list to find visible app packages by keyword and package_get to read package detail before editing; if creating or updating an app package may be appropriate, use package_apply with explicit user intent; otherwise use app_resolve to locate app resources, "
39
+ "Use workspace_icon_catalog_get before creating app packages, apps, or portals when supported icon/color candidates are needed; new workspace resources require explicit non-template icon + color, and the CLI validates choices without inferring business defaults. "
39
40
  "app_get as the default app map read, then app_get_fields/app_repair_code_blocks/app_get_layout/app_get_views/app_get_flow/app_get_charts/portal_list/portal_get/view_get/chart_get for focused configuration reads, "
40
41
  "member_search/role_search/role_create when workflow assignees must come from the directory or role catalog, preferring roles over explicit members unless the user explicitly names members, "
41
- "then app_schema_apply/app_layout_apply/app_flow_apply/app_views_apply/app_custom_buttons_apply/app_associated_resources_apply/app_charts_apply/portal_apply to execute normalized patches; these apply tools perform planning, normalization, and dependency checks internally where applicable. Schema/layout/views noop requests skip publish, app_custom_buttons_apply and app_associated_resources_apply publish after at least one write succeeds and expose no draft-only parameter, charts are immediate-live without publish and resolve targets by chart_id first then exact unique chart name, portal updates use replace semantics only when sections are supplied and edit-mode base-info-only updates may omit sections, publish=false only guarantees draft/base-info updates for tools that still expose that parameter, and flow should use publish=false whenever you only want draft/precheck behavior. "
42
+ "then app_schema_apply/app_layout_apply/app_flow_apply/app_views_apply/app_custom_buttons_apply/app_associated_resources_apply/app_charts_apply/portal_apply to execute normalized patches; these apply tools perform planning, normalization, and dependency checks internally where applicable. Schema/layout/views noop requests skip publish, app_custom_buttons_apply and app_associated_resources_apply publish after at least one write succeeds and expose no draft-only parameter, charts are immediate-live without publish and resolve targets by chart_id first then exact unique chart name, portal updates use replace semantics only when sections are supplied and edit-mode base-info-only updates may omit sections, portal pc layout is a 24-column grid and mobile is a 6-column grid so omit position or use layout_preset when unsure, publish=false only guarantees draft/base-info updates for tools that still expose that parameter, and flow should use publish=false whenever you only want draft/precheck behavior. "
42
43
  "Builder apply/write outputs include schema_version, operation, summary, and resources[]; use resources[].id/key/name/ids/parent as the stable UI and agent display entry, and keep legacy fields such as field_diff/views_diff/chart_results only for compatibility or troubleshooting. "
43
44
  "For existing object parameter replacement, prefer patch_views, patch_buttons, patch_resources, and patch_charts with set/unset; the tool reads current config and full-saves internally, while upsert_* is for creation or full target configuration and should not be used as an incomplete partial update. "
45
+ "For builder delete/remove apply results, separate delete execution from readback verification: after DELETE is sent, resources expose delete_executed, readback_status, and safe_to_retry_delete=false. If readback_status is unavailable or still_exists, do not blindly repeat the delete; confirm later with app_get/view_get/chart_get or the relevant apply readback. Views/buttons use single-item readback; associated resources use one app-level resource-pool readback because there is no confirmed single-item GET. "
44
46
  "For app_schema_apply, configure data title and data cover directly in field JSON with as_data_title=true and as_data_cover=true; data title is required and exactly one field may be marked, while data cover is optional and must be a top-level attachment field. For multi-app creation, pass apps[]/--apps-file on app_schema_apply; each item may have client_key, and relation fields may use target_app_ref to point at another same-call client_key. "
45
47
  "For app_views_apply, keep fixed saved filters in filters and configure the frontend query panel separately with query_conditions; query_conditions.rows is a matrix of field names compiled to backend queryCondition queIds. New views default associated report/view display to visible with limit_type=all; existing views preserve their current associated display unless associated_resources is explicitly patched. "
46
48
  "For custom button body create/update/delete and view placement, use app_custom_buttons_apply. For addData buttons, prefer trigger_add_data_config.target_app_key + field_mappings/default_values; do not ask agents to write raw que_relation unless maintaining a legacy config. field_mappings.source_field accepts source schema fields and supported system fields: 数据ID/row_record_id/apply_id/_id means current record id (-17), 编号/record_number means visible record number (0). To fill a target relation with the current record, map {'source_field': '数据ID', 'target_field': '目标引用字段'}; default_values is only for static constants. View button bindings merge by default and merge-mode view_configs must include buttons; use view_configs[].mode=replace or explicit buttons=[] only when clearing/replacing existing bindings is intended. Builder view_key arguments are raw keys from app_get.views[].view_key and must not be prefixed with custom:. "
47
- "For BI reports, keep report-body development separate from Qingflow in-app display: use app_charts_apply to create, update, remove, or reorder app-source QingBI chart bodies/configs with dataSourceType=qingflow; dataset BI reports are not created or edited by app_charts_apply yet and should be created in QingBI first, then attached with app_associated_resources_apply using report_source=dataset. "
49
+ "For BI reports, keep report-body development separate from Qingflow in-app display: use app_charts_apply to create, update, remove, or reorder app-source QingBI chart bodies/configs with dataSourceType=qingflow; chart dimension/metric/filter/query fields must come from app_get_fields.chart_fields, not record schema or form-only fields; dataset BI reports are not created or edited by app_charts_apply yet and should be created in QingBI first, then attached with app_associated_resources_apply using report_source=dataset. "
48
50
  "For associated views/reports, use app_associated_resources_apply. Use match_mappings for filtering associated resources: dynamic current-record conditions use source_field, static conditions use value. match_mappings also supports 数据ID(-17) and 编号(0). Do not ask agents to write raw match_rules unless preserving a legacy backend config. "
49
51
  "For associated reports/views, use app_associated_resources_apply for both the app-level associated_resources pool and per-view display config; associated_item_id is the app-level form_asos_chart.id, and view_configs/remove/reorder may also pass an existing resource's chart_id/chart_key/view_key because the tool resolves those to the internal id. Before creating an associated resource, read app_get.associated_resources and reuse an existing matching target_app_key + view_key/chart_key through patch_resources; client_key only works inside one apply call and is not persisted. Do not ask agents to pass backend raw sourceType: views infer the internal Qingflow view source, reports default to BI app reports, and dataset reports use report_source=dataset. "
50
52
  "For code_block fields with output bindings, always use qf_output assignment rather than const/let qf_output, and use app_repair_code_blocks when an existing form hangs because output-bound fields stay loading. "
@@ -177,6 +179,14 @@ def build_builder_server() -> FastMCP:
177
179
  def builder_tool_contract(tool_name: str = "") -> dict:
178
180
  return ai_builder.builder_tool_contract(tool_name=tool_name)
179
181
 
182
+ @server.tool()
183
+ def workspace_icon_catalog_get(profile: str = DEFAULT_PROFILE) -> dict:
184
+ return ai_builder.workspace_icon_catalog_get(profile=profile)
185
+
186
+ @server.tool()
187
+ def package_list(profile: str = DEFAULT_PROFILE, trial_status: str = "all", query: str = "") -> dict:
188
+ return ai_builder.package_list(profile=profile, trial_status=trial_status, query=query)
189
+
180
190
  @server.tool()
181
191
  def package_get(profile: str = DEFAULT_PROFILE, package_id: int = 0) -> dict:
182
192
  return ai_builder.package_get(profile=profile, package_id=package_id)
@@ -533,9 +543,12 @@ def build_builder_server() -> FastMCP:
533
543
  profile: str = DEFAULT_PROFILE,
534
544
  dash_key: str = "",
535
545
  dash_name: str = "",
546
+ name: str = "",
536
547
  package_id: int | None = None,
537
548
  publish: bool = True,
538
549
  sections: list[dict] | None = None,
550
+ pages: list[dict] | None = None,
551
+ layout_preset: str = "",
539
552
  visibility: dict | None = None,
540
553
  auth: dict | None = None,
541
554
  icon: str | None = None,
@@ -543,10 +556,14 @@ def build_builder_server() -> FastMCP:
543
556
  hide_copyright: bool | None = None,
544
557
  dash_global_config: dict | None = None,
545
558
  config: dict | None = None,
559
+ payload: dict | None = None,
546
560
  ) -> dict:
561
+ payload = payload if isinstance(payload, dict) else {}
547
562
  has_dash_key = bool((dash_key or "").strip())
548
- has_dash_name = bool((dash_name or "").strip())
549
- has_package_id = package_id is not None
563
+ effective_dash_name = (dash_name or name or str(payload.get("dash_name") or payload.get("dashName") or payload.get("name") or "")).strip()
564
+ has_dash_name = bool(effective_dash_name)
565
+ effective_package_id = package_id if package_id is not None else payload.get("package_id") or payload.get("packageId") or payload.get("package_tag_id")
566
+ has_package_id = effective_package_id is not None
550
567
  if has_dash_key and has_package_id:
551
568
  return _config_failure(
552
569
  "portal_apply accepts exactly one selector mode.",
@@ -561,9 +578,12 @@ def build_builder_server() -> FastMCP:
561
578
  profile=profile,
562
579
  dash_key=dash_key,
563
580
  dash_name=dash_name,
581
+ name=name,
564
582
  package_id=package_id,
565
583
  publish=publish,
566
584
  sections=sections or [],
585
+ pages=pages or [],
586
+ layout_preset=layout_preset,
567
587
  visibility=visibility,
568
588
  auth=auth,
569
589
  icon=icon,
@@ -571,6 +591,7 @@ def build_builder_server() -> FastMCP:
571
591
  hide_copyright=hide_copyright,
572
592
  dash_global_config=dash_global_config,
573
593
  config=config or {},
594
+ payload=payload,
574
595
  )
575
596
 
576
597
  @server.tool()
@@ -31,7 +31,7 @@ def build_user_server() -> FastMCP:
31
31
 
32
32
  ## App Discovery
33
33
 
34
- If `app_key` is unknown, use `app_list` or `app_search` first.
34
+ If `app_key` is unknown, use `app_list` first. Pass `query` to filter visible apps by keyword.
35
35
  If the app is known but the data range is not, use `app_get` first and choose from `accessible_views`.
36
36
  If an accessible view has `analysis_supported=false`, do not use it for `record_access` or `record_list`. `boardView` and `ganttView` are special UI views, not data-access targets.
37
37
  `view_get(view_id=...)` also returns `export_capability`; it only means there is a supported export route, not that export permission has been verified.
@@ -47,9 +47,9 @@ If an accessible view has `analysis_supported=false`, do not use it for `record_
47
47
  ## Schema-First Rule
48
48
 
49
49
  Call `record_insert_schema_get` before `record_insert`.
50
- Call `record_update_schema_get` before `record_update`.
50
+ For simple field changes after the target record is clear, call `record_update` directly. Use `record_update_schema_get` for diagnostics, ambiguous fields, or complex writable-scope inspection.
51
51
  Call `record_code_block_schema_get` before `record_code_block_run`.
52
- Call `app_get` first when the data range is unclear, then use `record_browse_schema_get(view_id=...)` before `record_access`, `record_list`, or `record_get`.
52
+ Call `app_get` first when the data range is unclear, then use `record_browse_schema_get(view_id=...)` before `record_access`, `record_list`, `record_get`, or `record_logs_get`.
53
53
  Call `record_import_schema_get` when the import field mapping is unclear before template download or verify.
54
54
 
55
55
  - All `field_id` values must come from the schema response.
@@ -59,7 +59,7 @@ Call `record_import_schema_get` when the import field mapping is unclear before
59
59
 
60
60
  `record_insert_schema_get` returns the current user's insert-ready applicant schema; read `required_fields`, `optional_fields`, `runtime_linked_required_fields`, and `payload_template`.
61
61
  Inside `optional_fields`, any field with `may_become_required=true` is still writable, but may become required when linked visibility or option-driven runtime rules activate.
62
- `record_update_schema_get` returns the current record's overall update-ready writable field set across matched accessible views; read `writable_fields` and `payload_template`.
62
+ `record_update_schema_get` returns the current record's overall update-ready writable field set and route diagnostics across matched accessible views; read `writable_fields`, `payload_template`, `available_update_routes`, and `recommended_update_route`.
63
63
  `record_browse_schema_get(view_id=...)` returns the same readable fields shown in the selected Qingflow table view header.
64
64
  `record_access.fields` / CSV columns and `record_list.columns / where / order_by / query_fields` use that exact same view schema; a missing field means it is not readable in that view.
65
65
  `searchQueIds` is a backend full-text search scope, not an output-column/projection mechanism.
@@ -102,9 +102,9 @@ Analysis answers must include concrete numbers. When applicable, include percent
102
102
 
103
103
  ## Record CRUD Path
104
104
 
105
- `app_get -> record_browse_schema_get(view_id=...) -> record_list / record_get`
105
+ `app_get -> record_browse_schema_get(view_id=...) -> record_list / record_get / record_logs_get`
106
106
  `record_insert_schema_get -> record_insert(items)`
107
- `record_update_schema_get -> record_update`
107
+ `record_update` for simple updates; `record_update_schema_get -> record_update` when the writable field scope is unclear.
108
108
  `record_list / record_get -> record_delete`
109
109
  `record_code_block_schema_get -> record_code_block_run`
110
110
  `portal_list -> portal_get -> chart_get / view_get`
@@ -117,15 +117,16 @@ Analysis answers must include concrete numbers. When applicable, include percent
117
117
  - Legacy forms such as bare integer `field_id`, `fieldId`, `operator`, `values`, or `order` may still parse, but they are compatibility-only and not the canonical DSL
118
118
 
119
119
  - `record_insert` defaults to an applicant-node `items` array; each item contains a field-title keyed `fields` map. A single insert is one item.
120
- - `record_update` uses a field-title keyed `fields` map and internally selects the first accessible view that can execute the current payload.
120
+ - `record_update` uses a field-title keyed `fields` map. It first tries the data-manager direct update route, then falls back to the frontend custom-view detail edit route when the selected view can cover the payload; if a unique current-user todo task for the same record exposes editable fields, it can finally use the workflow save-only route. Read `update_route` and `tried_routes` after execution.
121
121
  - For insert, `runtime_linked_required_fields` means required-but-not-directly-writable fields that are usually supplied by runtime linkage or upstream context.
122
122
  - For insert, fields marked `may_become_required=true` stay in `optional_fields`; they are still directly writable, but linked visibility or option-driven rules can make them required at runtime.
123
123
  - Read field-level `linkage` whenever present on `record_insert_schema_get` or `record_update_schema_get`; it is the static hint for linked visibility, reference-driven auto fill, and formula/default auto-fill behavior.
124
124
  - `linkage.sources` lists upstream field titles that influence the current field; `linkage.affects_fields` lists downstream fields that may change when the current field changes.
125
125
  - `linkage.kind=logic_visibility` means linked visibility or option-driven rules are involved; `linkage.kind=reference_fill` means reference/default matching logic is involved; `linkage.kind=formula_fill` means formula/default auto-fill logic is involved.
126
- - `record_update_schema_get` exposes the overall writable field set for the record, but not every field combination is guaranteed; `record_update` still needs one single matched accessible view that can cover the payload.
126
+ - `record_update_schema_get` exposes the overall writable field set and update route candidates for the record, but not every field combination is guaranteed; `record_update` still needs data-manager permission, one single matched custom view that can cover the payload, or one unique editable current-user todo task.
127
127
  - `record_delete` deletes by `record_id` or `record_ids`.
128
128
  - `record_get` is the single-record frontend detail context tool. It returns detail-page visible fields, one-level relation targets, first-page data/workflow logs, associated views/reports, local readable image assets, local downloadable file assets, unavailable context, and `semantic_context`.
129
+ - Use `record_logs_get` only when the user needs the full visible data/workflow log history for a specific record. It writes JSONL files locally and returns file paths plus completeness metadata; do not expect full log arrays in the response.
129
130
  - Read record images from `record_get.media_assets.items[].local_path` when `readable_by_agent=true`; read attachments/documents/tables from `record_get.file_assets.items[].local_path` and `extraction.text_path` when present. `record_get` follows the frontend storage cookie redirect path for Qingflow attachments, and remote file URLs should not be treated as directly readable.
130
131
  - `record_get.columns` are focus hints only; they do not project the detail fields. Read facts from top-level `fields[]`.
131
132
  - When readback shape matters after insert or update, prefer `record_get` for human/detail-page context, or `record_list(..., output_profile="normalized")` for batch-shaped normalized rows.
@@ -190,7 +191,7 @@ Use export only when the user explicitly asks to export/download/generate an Exc
190
191
  - `task_action_execute(task_id=..., action=...)` is also supported; MCP resolves the current todo locator internally before calling the real action route.
191
192
  - `task_workflow_log_get(task_id=...)` and `task_associated_report_detail_get(task_id=...)` are also supported for the current todo context.
192
193
  - Use `task_associated_report_detail_get` for associated view or report details.
193
- - Use `task_workflow_log_get` for full workflow log history.
194
+ - Use `task_workflow_log_get` for the current task context workflow log page. For full record-level data/workflow logs, use `record_logs_get(app_key, record_id, view_id?)`.
194
195
  - Task actions operate on `app_key + record_id + workflow_node_id`, not `task_id`.
195
196
  - Treat `task_action_execute` as the tool-level action enum surface; the current task's real actions are only the ones listed in `task_get.capabilities.available_actions`.
196
197
  - Use `task_action_execute(action="save_only", fields=...)` when the user wants to save editable field changes on the current node without advancing the workflow.
@@ -360,12 +361,8 @@ If the current MCP capability is unsupported, the workflow is awkward, or the us
360
361
  )
361
362
 
362
363
  @server.tool()
363
- def app_list(profile: str = DEFAULT_PROFILE) -> dict:
364
- return apps.app_list(profile=profile)
365
-
366
- @server.tool()
367
- def app_search(profile: str = DEFAULT_PROFILE, keyword: str = "", page_num: int = 1, page_size: int = 50) -> dict:
368
- return apps.app_search(profile=profile, keyword=keyword, page_num=page_num, page_size=page_size)
364
+ def app_list(profile: str = DEFAULT_PROFILE, query: str = "", keyword: str = "") -> dict:
365
+ return apps.app_list(profile=profile, query=query, keyword=keyword)
369
366
 
370
367
  @server.tool()
371
368
  def app_get(profile: str = DEFAULT_PROFILE, app_key: str = "") -> dict: