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

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.
Files changed (89) hide show
  1. package/README.md +9 -3
  2. package/docs/local-agent-install.md +54 -3
  3. package/entry_point.py +1 -1
  4. package/npm/bin/qingflow-skills.mjs +5 -0
  5. package/npm/lib/runtime.mjs +304 -13
  6. package/npm/scripts/postinstall.mjs +1 -5
  7. package/package.json +3 -2
  8. package/pyproject.toml +1 -1
  9. package/skills/qingflow-app-builder/SKILL.md +255 -0
  10. package/skills/qingflow-app-builder/agents/openai.yaml +4 -0
  11. package/skills/qingflow-app-builder/references/create-app.md +149 -0
  12. package/skills/qingflow-app-builder/references/environments.md +63 -0
  13. package/skills/qingflow-app-builder/references/flow-actors-and-permissions.md +123 -0
  14. package/skills/qingflow-app-builder/references/gotchas.md +107 -0
  15. package/skills/qingflow-app-builder/references/match-rules.md +114 -0
  16. package/skills/qingflow-app-builder/references/public-surface-sync.md +75 -0
  17. package/skills/qingflow-app-builder/references/solution-playbooks.md +52 -0
  18. package/skills/qingflow-app-builder/references/tool-selection.md +99 -0
  19. package/skills/qingflow-app-builder/references/update-flow.md +158 -0
  20. package/skills/qingflow-app-builder/references/update-layout.md +68 -0
  21. package/skills/qingflow-app-builder/references/update-schema.md +72 -0
  22. package/skills/qingflow-app-builder/references/update-views.md +284 -0
  23. package/skills/qingflow-app-builder-code-integrations/SKILL.md +137 -0
  24. package/skills/qingflow-app-builder-code-integrations/agents/openai.yaml +4 -0
  25. package/skills/qingflow-app-builder-code-integrations/references/code-block.md +66 -0
  26. package/skills/qingflow-app-builder-code-integrations/references/q-linker.md +77 -0
  27. package/skills/qingflow-app-user/SKILL.md +12 -11
  28. package/skills/qingflow-app-user/references/data-gotchas.md +2 -2
  29. package/skills/qingflow-app-user/references/public-surface-sync.md +3 -3
  30. package/skills/qingflow-app-user/references/record-patterns.md +5 -5
  31. package/skills/qingflow-app-user/references/workflow-usage.md +4 -5
  32. package/skills/qingflow-mcp-setup/SKILL.md +113 -0
  33. package/skills/qingflow-mcp-setup/agents/openai.yaml +4 -0
  34. package/skills/qingflow-mcp-setup/references/claude-desktop.md +34 -0
  35. package/skills/qingflow-mcp-setup/references/environments.md +62 -0
  36. package/skills/qingflow-mcp-setup/references/generic-stdio.md +32 -0
  37. package/skills/qingflow-mcp-setup/scripts/check_local_server.sh +38 -0
  38. package/skills/qingflow-record-analysis/SKILL.md +6 -7
  39. package/skills/qingflow-record-analysis/manifest.yaml +10 -0
  40. package/skills/qingflow-record-delete/SKILL.md +5 -3
  41. package/skills/qingflow-record-import/SKILL.md +6 -2
  42. package/skills/qingflow-record-insert/SKILL.md +48 -4
  43. package/skills/qingflow-record-insert/manifest.yaml +6 -0
  44. package/skills/qingflow-record-update/SKILL.md +36 -24
  45. package/skills/qingflow-task-ops/SKILL.md +25 -25
  46. package/skills/qingflow-task-ops/references/environments.md +0 -1
  47. package/skills/qingflow-task-ops/references/workflow-usage.md +4 -6
  48. package/src/qingflow_mcp/__main__.py +6 -2
  49. package/src/qingflow_mcp/builder_facade/models.py +41 -2
  50. package/src/qingflow_mcp/builder_facade/service.py +2743 -423
  51. package/src/qingflow_mcp/cli/commands/app.py +3 -16
  52. package/src/qingflow_mcp/cli/commands/builder.py +30 -4
  53. package/src/qingflow_mcp/cli/commands/exports.py +2 -2
  54. package/src/qingflow_mcp/cli/commands/imports.py +1 -1
  55. package/src/qingflow_mcp/cli/commands/record.py +54 -11
  56. package/src/qingflow_mcp/cli/context.py +0 -3
  57. package/src/qingflow_mcp/cli/formatters.py +238 -8
  58. package/src/qingflow_mcp/cli/main.py +47 -3
  59. package/src/qingflow_mcp/errors.py +43 -2
  60. package/src/qingflow_mcp/public_surface.py +24 -16
  61. package/src/qingflow_mcp/response_trim.py +119 -12
  62. package/src/qingflow_mcp/server.py +17 -14
  63. package/src/qingflow_mcp/server_app_builder.py +29 -7
  64. package/src/qingflow_mcp/server_app_user.py +23 -24
  65. package/src/qingflow_mcp/solution/compiler/icon_utils.py +294 -0
  66. package/src/qingflow_mcp/solution/executor.py +112 -15
  67. package/src/qingflow_mcp/tools/ai_builder_tools.py +497 -65
  68. package/src/qingflow_mcp/tools/app_tools.py +237 -51
  69. package/src/qingflow_mcp/tools/approval_tools.py +196 -34
  70. package/src/qingflow_mcp/tools/auth_tools.py +92 -16
  71. package/src/qingflow_mcp/tools/code_block_tools.py +296 -39
  72. package/src/qingflow_mcp/tools/custom_button_tools.py +64 -10
  73. package/src/qingflow_mcp/tools/directory_tools.py +236 -72
  74. package/src/qingflow_mcp/tools/export_tools.py +230 -33
  75. package/src/qingflow_mcp/tools/file_tools.py +7 -3
  76. package/src/qingflow_mcp/tools/import_tools.py +293 -40
  77. package/src/qingflow_mcp/tools/navigation_tools.py +91 -12
  78. package/src/qingflow_mcp/tools/package_tools.py +134 -8
  79. package/src/qingflow_mcp/tools/portal_tools.py +39 -3
  80. package/src/qingflow_mcp/tools/qingbi_report_tools.py +116 -7
  81. package/src/qingflow_mcp/tools/record_tools.py +2305 -442
  82. package/src/qingflow_mcp/tools/resource_read_tools.py +191 -39
  83. package/src/qingflow_mcp/tools/role_tools.py +80 -9
  84. package/src/qingflow_mcp/tools/solution_tools.py +57 -15
  85. package/src/qingflow_mcp/tools/task_context_tools.py +569 -119
  86. package/src/qingflow_mcp/tools/task_tools.py +113 -29
  87. package/src/qingflow_mcp/tools/view_tools.py +106 -3
  88. package/src/qingflow_mcp/tools/workflow_tools.py +17 -1
  89. package/src/qingflow_mcp/tools/workspace_tools.py +71 -3
@@ -47,7 +47,6 @@ COMMON_ERROR_DROP_TOP = {
47
47
  "allowed_values",
48
48
  "missing_fields",
49
49
  "noop",
50
- "ok",
51
50
  }
52
51
 
53
52
  SUCCESS_POLICY_BY_TOOL: dict[str, TransformFn] = {}
@@ -58,11 +57,18 @@ def trim_public_response(tool_name: str | None, payload: dict[str, Any]) -> dict
58
57
  return payload
59
58
  if _looks_like_failure_payload(payload):
60
59
  status = str(payload.get("status") or "").lower()
61
- if tool_name in {"user:record_insert", "user:record_update", "user:record_delete"} and status in {
62
- "blocked",
63
- "needs_confirmation",
64
- "partial_success",
65
- }:
60
+ if _is_executed_nonfatal_payload(payload) or (
61
+ tool_name in {"user:record_insert", "user:record_update", "user:record_delete"}
62
+ and status in {
63
+ "blocked",
64
+ "needs_confirmation",
65
+ "partial_success",
66
+ }
67
+ ) or (
68
+ tool_name == "user:record_import_verify"
69
+ and status == "failed"
70
+ and "can_import" in payload
71
+ ):
66
72
  return trim_success_response(tool_name, payload)
67
73
  return _trim_returned_failure(payload)
68
74
  return trim_success_response(tool_name, payload)
@@ -73,7 +79,7 @@ def trim_success_response(tool_name: str | None, payload: dict[str, Any]) -> dic
73
79
  return payload
74
80
  trimmed = deepcopy(payload)
75
81
  drop_keys = COMMON_SUCCESS_DROP_TOP
76
- if tool_name == "user:record_get":
82
+ if tool_name in {"user:record_get", "user:record_logs_get"}:
77
83
  drop_keys = COMMON_SUCCESS_DROP_TOP - {"output_profile"}
78
84
  if tool_name in {"user:record_insert", "user:record_update", "user:record_delete"} and payload.get("ok") is False:
79
85
  drop_keys = drop_keys - {"ok"}
@@ -151,12 +157,25 @@ def _parse_runtime_error_payload(exc: RuntimeError) -> dict[str, Any] | None:
151
157
 
152
158
 
153
159
  def _looks_like_failure_payload(payload: dict[str, Any]) -> bool:
160
+ if _is_executed_nonfatal_payload(payload):
161
+ return False
154
162
  if payload.get("ok") is False:
155
163
  return True
156
164
  status = str(payload.get("status") or "").lower()
157
165
  return status in {"failed", "blocked"}
158
166
 
159
167
 
168
+ def _is_executed_nonfatal_payload(payload: dict[str, Any]) -> bool:
169
+ status = str(payload.get("status") or "").lower()
170
+ executed = bool(
171
+ payload.get("write_executed")
172
+ or payload.get("delete_executed")
173
+ or payload.get("action_executed")
174
+ or payload.get("export_executed")
175
+ )
176
+ return executed and status in {"partial_success", "verification_failed", "running", "queued", "unknown"}
177
+
178
+
160
179
  def _trim_returned_failure(payload: dict[str, Any]) -> dict[str, Any]:
161
180
  trimmed = deepcopy(payload)
162
181
  _drop_top_keys(trimmed, COMMON_ERROR_DROP_TOP)
@@ -294,7 +313,7 @@ def _trim_workspace_get(payload: JSONObject) -> None:
294
313
  )
295
314
 
296
315
 
297
- def _trim_app_search_like(payload: JSONObject) -> None:
316
+ def _trim_app_list_like(payload: JSONObject) -> None:
298
317
  payload.pop("apps", None)
299
318
  _trim_item_list(payload, "items", allowed=("app_key", "app_name", "package_name"))
300
319
 
@@ -325,15 +344,22 @@ def _trim_import_schema(payload: JSONObject) -> None:
325
344
  columns = [item for item in payload.get("expected_columns", []) if isinstance(item, dict)]
326
345
  if columns is not None:
327
346
  payload["columns"] = [_compact_import_column(item) for item in columns]
347
+ import_capability = payload.get("import_capability") if isinstance(payload.get("import_capability"), dict) else {}
348
+ if import_capability:
349
+ verification = payload.get("verification") if isinstance(payload.get("verification"), dict) else {}
350
+ if not isinstance(payload.get("verification"), dict):
351
+ payload["verification"] = verification
352
+ verification.setdefault("import_auth_source", import_capability.get("auth_source"))
353
+ verification.setdefault("import_capability_can_import", import_capability.get("can_import"))
328
354
  payload.pop("expected_columns", None)
329
355
  payload.pop("schema_fingerprint", None)
330
356
  payload.pop("import_capability", None)
331
357
  payload.pop("request_route", None)
332
- payload.pop("verification", None)
333
358
 
334
359
  if _looks_like_import_verify(payload):
335
360
  _trim_import_verify_payload(payload)
336
361
  return
362
+ payload.pop("verification", None)
337
363
  if "applied_repairs" in payload or "repaired_file_path" in payload:
338
364
  _trim_import_repair_payload(payload)
339
365
  return
@@ -394,6 +420,14 @@ def _trim_record_write(payload: JSONObject) -> None:
394
420
  data.pop("normalized_payload", None)
395
421
  data.pop("human_review", None)
396
422
  data.pop("action", None)
423
+ if payload.get("status") == "success":
424
+ data.pop("tried_routes", None)
425
+ update_route = payload.get("update_route")
426
+ if update_route not in (None, [], {}, ""):
427
+ data["update_route"] = update_route
428
+ tried_routes = payload.get("tried_routes")
429
+ if payload.get("status") != "success" and tried_routes not in (None, [], {}, ""):
430
+ data["tried_routes"] = tried_routes
397
431
  resource = _compact_record_resource(data.get("resource"))
398
432
  if resource:
399
433
  data["resource"] = resource
@@ -437,8 +471,11 @@ def _trim_record_write_batch(payload: JSONObject, data: JSONObject) -> None:
437
471
  "record_id",
438
472
  "apply_id",
439
473
  "write_executed",
474
+ "write_succeeded",
440
475
  "verification_status",
441
476
  "safe_to_retry",
477
+ "update_route",
478
+ "tried_routes",
442
479
  "failed_fields",
443
480
  "confirmation_requests",
444
481
  "blockers",
@@ -668,6 +705,49 @@ def _trim_record_access(payload: JSONObject) -> None:
668
705
  payload.update(compact)
669
706
 
670
707
 
708
+ def _trim_record_logs(payload: JSONObject) -> None:
709
+ compact: dict[str, Any] = {}
710
+ for key in (
711
+ "ok",
712
+ "status",
713
+ "output_profile",
714
+ "app",
715
+ "view",
716
+ "record",
717
+ "local_dir",
718
+ "summary_path",
719
+ "warnings",
720
+ "unavailable_context",
721
+ "context_integrity",
722
+ ):
723
+ value = payload.get(key)
724
+ if value is not None:
725
+ compact[key] = value
726
+ for key in ("data_logs", "workflow_logs"):
727
+ node = payload.get(key)
728
+ if isinstance(node, dict):
729
+ compact[key] = _pick(
730
+ node,
731
+ (
732
+ "status",
733
+ "visible",
734
+ "source",
735
+ "reason",
736
+ "complete",
737
+ "items_count",
738
+ "pages_fetched",
739
+ "page_size",
740
+ "reported_total",
741
+ "local_path",
742
+ "preview_items",
743
+ "warnings",
744
+ "stopped_reason",
745
+ ),
746
+ )
747
+ payload.clear()
748
+ payload.update(compact)
749
+
750
+
671
751
  def _trim_record_analyze(payload: JSONObject) -> None:
672
752
  summary: dict[str, Any] = {}
673
753
  completeness = payload.get("completeness")
@@ -853,13 +933,37 @@ def _compact_import_column(item: dict[str, Any]) -> dict[str, Any]:
853
933
 
854
934
 
855
935
  def _looks_like_import_verify(payload: JSONObject) -> bool:
856
- return "verification_id" in payload and "can_import" in payload
936
+ if "verification_id" in payload and "can_import" in payload:
937
+ return True
938
+ return "can_import" in payload and (
939
+ "error_code" in payload
940
+ or "issues" in payload
941
+ or "file_path" in payload
942
+ )
857
943
 
858
944
 
859
945
  def _trim_import_verify_payload(payload: JSONObject) -> None:
860
946
  issues = payload.get("issues") if isinstance(payload.get("issues"), list) else []
861
947
  issue_summary = _summarize_import_issues(issues)
862
948
  payload["issue_summary"] = issue_summary
949
+ verification = payload.get("verification") if isinstance(payload.get("verification"), dict) else {}
950
+ compact_verification = {
951
+ key: verification.get(key)
952
+ for key in (
953
+ "import_auth_prechecked",
954
+ "import_auth_precheck_passed",
955
+ "import_auth_source",
956
+ "import_capability_can_import",
957
+ "local_precheck_passed",
958
+ "backend_verification_passed",
959
+ "auto_normalized",
960
+ )
961
+ if key in verification
962
+ }
963
+ if compact_verification:
964
+ payload["verification"] = compact_verification
965
+ else:
966
+ payload.pop("verification", None)
863
967
  columns = payload.get("columns")
864
968
  if "expected_columns" not in payload and isinstance(columns, list):
865
969
  payload["expected_columns"] = columns
@@ -955,7 +1059,7 @@ def _trim_feedback(payload: JSONObject) -> None:
955
1059
 
956
1060
 
957
1061
  def _trim_builder_envelope(payload: JSONObject) -> None:
958
- if str(payload.get("status") or "").lower() == "success":
1062
+ if str(payload.get("status") or "").lower() in {"success", "partial_success", "verification_failed"}:
959
1063
  details = payload.get("details")
960
1064
  if isinstance(details, dict):
961
1065
  _drop_deep_keys(details, {"request_route", "base_url", "normalized_args", "suggested_next_call", "transport", "response", "body", "raw"})
@@ -984,7 +1088,7 @@ _register_policy((USER_DOMAIN, BUILDER_DOMAIN), ("auth_use_credential", "auth_wh
984
1088
  _register_policy((USER_DOMAIN, BUILDER_DOMAIN), ("auth_logout",), _trim_auth_logout)
985
1089
  _register_policy((USER_DOMAIN, BUILDER_DOMAIN), ("workspace_list",), _trim_workspace_list)
986
1090
  _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)
1091
+ _register_policy((USER_DOMAIN,), ("app_list",), _trim_app_list_like)
988
1092
  _register_policy((USER_DOMAIN, BUILDER_DOMAIN), ("app_get",), _trim_app_get)
989
1093
  _register_policy((BUILDER_DOMAIN,), ("app_repair_code_blocks",), _trim_builder_list_like)
990
1094
  _register_policy((USER_DOMAIN, BUILDER_DOMAIN), ("portal_list", "portal_get", "view_get", "chart_get"), _trim_builder_list_like)
@@ -1027,6 +1131,7 @@ _register_policy((USER_DOMAIN,), ("record_insert", "record_update"), _trim_recor
1027
1131
  _register_policy((USER_DOMAIN,), ("record_get",), _trim_record_get)
1028
1132
  _register_policy((USER_DOMAIN,), ("record_list",), _trim_record_list)
1029
1133
  _register_policy((USER_DOMAIN,), ("record_access",), _trim_record_access)
1134
+ _register_policy((USER_DOMAIN,), ("record_logs_get",), _trim_record_logs)
1030
1135
  _register_policy((USER_DOMAIN,), ("record_analyze",), _trim_record_analyze)
1031
1136
  _register_policy((USER_DOMAIN,), ("record_code_block_run",), _trim_code_block_run)
1032
1137
  _register_policy((USER_DOMAIN,), ("task_list",), _trim_task_list)
@@ -1067,6 +1172,8 @@ _register_policy(
1067
1172
  (BUILDER_DOMAIN,),
1068
1173
  (
1069
1174
  "builder_tool_contract",
1175
+ "workspace_icon_catalog_get",
1176
+ "package_list",
1070
1177
  "package_get",
1071
1178
  "package_apply",
1072
1179
  "solution_install",
@@ -48,17 +48,18 @@ 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
+ Treat an explicit `view_id` as the exact frontend view context. If `system:all` fails, do not silently switch to `system:initiated`, `system:todo`, or another system view unless the user, frontend URL, or `app_get.accessible_views` explicitly selects that view.
53
54
  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
55
  `view_get(view_id=...)` also returns `export_capability`; it only means there is a supported export route, not that export permission has been verified.
55
56
 
56
57
  ## Schema-First Rule
57
58
 
58
59
  Call `record_insert_schema_get` before `record_insert`.
59
- Call `record_update_schema_get` before `record_update`.
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`.
60
+ 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.
61
+ Prefer `record_code_block_schema_get` before `record_code_block_run` when field selection or binding diagnostics are unclear; if the exact code-block field id is already known from record/task detail, run directly.
62
+ 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
63
  Call `record_import_schema_get` when the import field mapping is unclear before template download or verify.
63
64
 
64
65
  - All `field_id` values must come from the schema response.
@@ -67,7 +68,7 @@ Call `record_import_schema_get` when the import field mapping is unclear before
67
68
  ## Schema Scope
68
69
 
69
70
  `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`.
71
+ `record_update_schema_get` returns the current record's update-ready writable field set and route diagnostics. When `view_id` is explicit, it is the exact frontend view context and must not be silently replaced by another view.
71
72
  `record_browse_schema_get(view_id=...)` returns the same readable fields shown in the selected Qingflow table view header.
72
73
  `record_access.fields` / CSV columns and `record_list.columns / where / order_by / query_fields` use that exact same view schema.
73
74
  `record_code_block_schema_get` returns code-block-ready schema for exact code block field selection.
@@ -103,11 +104,11 @@ Analysis answers must include concrete numbers. When applicable, include percent
103
104
 
104
105
  ## Record CRUD Path
105
106
 
106
- `app_get -> record_browse_schema_get(view_id=...) -> record_list / record_get`
107
+ `app_get -> record_browse_schema_get(view_id=...) -> record_list / record_get / record_logs_get`
107
108
  `record_insert_schema_get -> record_insert(items)`
108
- `record_update_schema_get -> record_update`
109
- `record_list / record_get -> record_delete`
110
- `record_code_block_schema_get -> record_code_block_run`
109
+ `record_update` for simple updates; `record_update_schema_get -> record_update` when the writable field scope is unclear.
110
+ `record_list / record_get -> record_delete(system view_id)`
111
+ `record_code_block_run` directly when the exact code-block field is known; otherwise `record_code_block_schema_get -> record_code_block_run`
111
112
 
112
113
  - Use `columns` as `[{{field_id}}]`
113
114
  - Use `record_list(query=..., query_fields=[{{field_id}}])` for fuzzy single-record lookup, then follow `lookup.next_action`; `query_fields` is search scope and `columns` is display shape.
@@ -116,15 +117,16 @@ Analysis answers must include concrete numbers. When applicable, include percent
116
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
117
118
 
118
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.
119
- - `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. On success, read `status`, `update_route`, and `verification_status`; on failure, read the failure reason and route diagnostics.
120
121
  - For insert, `runtime_linked_required_fields` means required-but-not-directly-writable fields that are usually supplied by runtime linkage or upstream context.
121
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.
122
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.
123
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.
124
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.
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.
126
- - `record_delete` deletes by `record_id` or `record_ids`.
126
+ - `record_update_schema_get` exposes the writable field set and route candidates for the record; with an explicit `view_id`, diagnostics stay scoped to that selected frontend view. 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
+ - `record_delete` deletes by `record_id` or `record_ids`, and requires an accessible system `view_id`; use custom views only to locate records, not as the delete route.
127
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.
128
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.
129
131
  - `record_get.columns` are focus hints only; they do not project the detail fields. Read facts from top-level `fields[]`.
130
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.
@@ -139,7 +141,8 @@ Analysis answers must include concrete numbers. When applicable, include percent
139
141
 
140
142
  Use `record_code_block_run` when the user wants to execute a form code-block field against an existing record.
141
143
 
142
- - Always resolve the exact code-block field from `record_code_block_schema_get` first.
144
+ - Prefer resolving the exact code-block field from `record_code_block_schema_get`, but do not treat applicant-schema 40002 as final denial when record/task detail already exposes the code-block field id.
145
+ - `record_code_block_run` uses the record/task detail answers for the execution context; when applicant schema is unavailable it can still execute with a numeric code-block field id, but schema-bound relation writeback may be skipped.
143
146
  - Treat code-block execution as write-capable, not read-only.
144
147
  - If the code block is bound to relation outputs, Qingflow may calculate target answers and write them back automatically.
145
148
  - For safe debugging, pass `apply_writeback=false` and inspect the parsed alias results plus `relation.calculated_answers_preview` before allowing any writeback.
@@ -185,7 +188,7 @@ Use `record_code_block_run` when the user wants to execute a form code-block fie
185
188
  - `task_action_execute(task_id=..., action=...)` is also supported; MCP resolves the current todo locator internally before calling the real action route.
186
189
  - `task_workflow_log_get(task_id=...)` and `task_associated_report_detail_get(task_id=...)` are also supported for the current todo context.
187
190
  - Use `task_associated_report_detail_get` for associated view or report details.
188
- - Use `task_workflow_log_get` for full workflow log history.
191
+ - Use `task_workflow_log_get` for the current task context workflow log page. For full record-level data/workflow logs, first choose an accessible view with `app_get`, then call `record_logs_get(app_key, record_id, view_id)` with that same explicit `view_id`.
189
192
  - Task actions operate on `app_key + record_id + workflow_node_id`, not `task_id`.
190
193
 
191
194
  ## Time Handling
@@ -35,16 +35,19 @@ 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. "
46
+ "For builder write/apply results, separate write execution from final readback verification: status=partial_success with write_executed=true and safe_to_retry=false means the write was sent or succeeded, while final readback, publish verification, or metadata confirmation is pending or permission-restricted. Report it as written but unverified; do not repeat the same write blindly. "
44
47
  "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
- "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
- "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. "
48
+ "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. View writes use ViewManagementAuth (beingViewManageStatus), falling back to DataManageAuth when advanced app permissions are not enabled. "
49
+ "For custom button body create/update/delete and view placement, use app_custom_buttons_apply. Button body writes require EditAppAuth; view_configs placement writes require ViewManagementAuth, matching the backend viewConfig route. 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:. "
50
+ "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
51
  "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
52
  "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
53
  "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 +180,14 @@ def build_builder_server() -> FastMCP:
177
180
  def builder_tool_contract(tool_name: str = "") -> dict:
178
181
  return ai_builder.builder_tool_contract(tool_name=tool_name)
179
182
 
183
+ @server.tool()
184
+ def workspace_icon_catalog_get(profile: str = DEFAULT_PROFILE) -> dict:
185
+ return ai_builder.workspace_icon_catalog_get(profile=profile)
186
+
187
+ @server.tool()
188
+ def package_list(profile: str = DEFAULT_PROFILE, trial_status: str = "all", query: str = "") -> dict:
189
+ return ai_builder.package_list(profile=profile, trial_status=trial_status, query=query)
190
+
180
191
  @server.tool()
181
192
  def package_get(profile: str = DEFAULT_PROFILE, package_id: int = 0) -> dict:
182
193
  return ai_builder.package_get(profile=profile, package_id=package_id)
@@ -533,9 +544,12 @@ def build_builder_server() -> FastMCP:
533
544
  profile: str = DEFAULT_PROFILE,
534
545
  dash_key: str = "",
535
546
  dash_name: str = "",
547
+ name: str = "",
536
548
  package_id: int | None = None,
537
549
  publish: bool = True,
538
550
  sections: list[dict] | None = None,
551
+ pages: list[dict] | None = None,
552
+ layout_preset: str = "",
539
553
  visibility: dict | None = None,
540
554
  auth: dict | None = None,
541
555
  icon: str | None = None,
@@ -543,10 +557,14 @@ def build_builder_server() -> FastMCP:
543
557
  hide_copyright: bool | None = None,
544
558
  dash_global_config: dict | None = None,
545
559
  config: dict | None = None,
560
+ payload: dict | None = None,
546
561
  ) -> dict:
562
+ payload = payload if isinstance(payload, dict) else {}
547
563
  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
564
+ effective_dash_name = (dash_name or name or str(payload.get("dash_name") or payload.get("dashName") or payload.get("name") or "")).strip()
565
+ has_dash_name = bool(effective_dash_name)
566
+ 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")
567
+ has_package_id = effective_package_id is not None
550
568
  if has_dash_key and has_package_id:
551
569
  return _config_failure(
552
570
  "portal_apply accepts exactly one selector mode.",
@@ -561,9 +579,12 @@ def build_builder_server() -> FastMCP:
561
579
  profile=profile,
562
580
  dash_key=dash_key,
563
581
  dash_name=dash_name,
582
+ name=name,
564
583
  package_id=package_id,
565
584
  publish=publish,
566
585
  sections=sections or [],
586
+ pages=pages or [],
587
+ layout_preset=layout_preset,
567
588
  visibility=visibility,
568
589
  auth=auth,
569
590
  icon=icon,
@@ -571,6 +592,7 @@ def build_builder_server() -> FastMCP:
571
592
  hide_copyright=hide_copyright,
572
593
  dash_global_config=dash_global_config,
573
594
  config=config or {},
595
+ payload=payload,
574
596
  )
575
597
 
576
598
  @server.tool()
@@ -31,8 +31,9 @@ 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
+ Treat an explicit `view_id` as the exact frontend view context. If `system:all` fails, do not silently switch to `system:initiated`, `system:todo`, or another system view unless the user, frontend URL, or `app_get.accessible_views` explicitly selects that view.
36
37
  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
38
  `view_get(view_id=...)` also returns `export_capability`; it only means there is a supported export route, not that export permission has been verified.
38
39
 
@@ -47,9 +48,9 @@ If an accessible view has `analysis_supported=false`, do not use it for `record_
47
48
  ## Schema-First Rule
48
49
 
49
50
  Call `record_insert_schema_get` before `record_insert`.
50
- Call `record_update_schema_get` before `record_update`.
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`.
51
+ 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.
52
+ Prefer `record_code_block_schema_get` before `record_code_block_run` when field selection or binding diagnostics are unclear; if the exact code-block field id is already known from record/task detail, run directly.
53
+ 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
54
  Call `record_import_schema_get` when the import field mapping is unclear before template download or verify.
54
55
 
55
56
  - All `field_id` values must come from the schema response.
@@ -59,11 +60,11 @@ Call `record_import_schema_get` when the import field mapping is unclear before
59
60
 
60
61
  `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
62
  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`.
63
+ `record_update_schema_get` returns the current record's update-ready writable field set and route diagnostics. When `view_id` is explicit, it is the exact frontend view context and must not be silently replaced by another view.
63
64
  `record_browse_schema_get(view_id=...)` returns the same readable fields shown in the selected Qingflow table view header.
64
65
  `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
66
  `searchQueIds` is a backend full-text search scope, not an output-column/projection mechanism.
66
- `record_code_block_schema_get` returns code-block-ready schema for exact code block field selection.
67
+ `record_code_block_schema_get` returns code-block-ready schema for exact code block field selection when the applicant schema is readable.
67
68
  `record_import_schema_get` returns import-ready column metadata.
68
69
 
69
70
  - Hidden fields are omitted.
@@ -102,11 +103,11 @@ Analysis answers must include concrete numbers. When applicable, include percent
102
103
 
103
104
  ## Record CRUD Path
104
105
 
105
- `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`
106
107
  `record_insert_schema_get -> record_insert(items)`
107
- `record_update_schema_get -> record_update`
108
- `record_list / record_get -> record_delete`
109
- `record_code_block_schema_get -> record_code_block_run`
108
+ `record_update` for simple updates; `record_update_schema_get -> record_update` when the writable field scope is unclear.
109
+ `record_list / record_get -> record_delete(system view_id)`
110
+ `record_code_block_run` directly when the exact code-block field is known; otherwise `record_code_block_schema_get -> record_code_block_run`
110
111
  `portal_list -> portal_get -> chart_get / view_get`
111
112
  `portal_get -> view_get -> record_list`
112
113
 
@@ -117,15 +118,16 @@ Analysis answers must include concrete numbers. When applicable, include percent
117
118
  - 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
119
 
119
120
  - `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.
121
+ - `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. On success, read `status`, `update_route`, and `verification_status`; on failure, read the failure reason and route diagnostics.
121
122
  - For insert, `runtime_linked_required_fields` means required-but-not-directly-writable fields that are usually supplied by runtime linkage or upstream context.
122
123
  - 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
124
  - 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
125
  - `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
126
  - `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.
127
- - `record_delete` deletes by `record_id` or `record_ids`.
127
+ - `record_update_schema_get` exposes the writable field set and update route candidates for the record; with an explicit `view_id`, diagnostics stay scoped to that selected frontend view. 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.
128
+ - `record_delete` deletes by `record_id` or `record_ids`, and requires an accessible system `view_id`; use custom views only to locate records, not as the delete route.
128
129
  - `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`.
130
+ - 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
131
  - 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
132
  - `record_get.columns` are focus hints only; they do not project the detail fields. Read facts from top-level `fields[]`.
131
133
  - 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.
@@ -141,7 +143,8 @@ Analysis answers must include concrete numbers. When applicable, include percent
141
143
 
142
144
  Use `record_code_block_run` when the user wants to execute a form code-block field against an existing record.
143
145
 
144
- - Always resolve the exact code-block field from `record_code_block_schema_get` first.
146
+ - Prefer resolving the exact code-block field from `record_code_block_schema_get`, but do not treat applicant-schema 40002 as final denial when record/task detail already exposes the code-block field id.
147
+ - `record_code_block_run` uses the record/task detail answers for the execution context; when applicant schema is unavailable it can still execute with a numeric code-block field id, but schema-bound relation writeback may be skipped.
145
148
  - Treat code-block execution as write-capable, not read-only.
146
149
  - If the code block is bound to relation outputs, Qingflow may calculate target answers and write them back automatically.
147
150
  - For safe debugging, pass `apply_writeback=false` and inspect the parsed alias results plus `relation.calculated_answers_preview` before allowing any writeback.
@@ -190,7 +193,7 @@ Use export only when the user explicitly asks to export/download/generate an Exc
190
193
  - `task_action_execute(task_id=..., action=...)` is also supported; MCP resolves the current todo locator internally before calling the real action route.
191
194
  - `task_workflow_log_get(task_id=...)` and `task_associated_report_detail_get(task_id=...)` are also supported for the current todo context.
192
195
  - Use `task_associated_report_detail_get` for associated view or report details.
193
- - Use `task_workflow_log_get` for full workflow log history.
196
+ - Use `task_workflow_log_get` for the current task context workflow log page. For full record-level data/workflow logs, first choose an accessible view with `app_get`, then call `record_logs_get(app_key, record_id, view_id)` with that same explicit `view_id`.
194
197
  - Task actions operate on `app_key + record_id + workflow_node_id`, not `task_id`.
195
198
  - 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
199
  - 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.
@@ -262,7 +265,7 @@ If the current MCP capability is unsupported, the workflow is awkward, or the us
262
265
  def record_export_start(
263
266
  profile: str = DEFAULT_PROFILE,
264
267
  app_key: str = "",
265
- view_id: str = "system:all",
268
+ view_id: str = "",
266
269
  columns: list[dict | int] | None = None,
267
270
  where: list[dict] | None = None,
268
271
  order_by: list[dict] | None = None,
@@ -303,7 +306,7 @@ If the current MCP capability is unsupported, the workflow is awkward, or the us
303
306
  def record_export_direct(
304
307
  profile: str = DEFAULT_PROFILE,
305
308
  app_key: str = "",
306
- view_id: str = "system:all",
309
+ view_id: str = "",
307
310
  columns: list[dict | int] | None = None,
308
311
  where: list[dict] | None = None,
309
312
  order_by: list[dict] | None = None,
@@ -360,12 +363,8 @@ If the current MCP capability is unsupported, the workflow is awkward, or the us
360
363
  )
361
364
 
362
365
  @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)
366
+ def app_list(profile: str = DEFAULT_PROFILE, query: str = "", keyword: str = "") -> dict:
367
+ return apps.app_list(profile=profile, query=query, keyword=keyword)
369
368
 
370
369
  @server.tool()
371
370
  def app_get(profile: str = DEFAULT_PROFILE, app_key: str = "") -> dict:
@@ -477,7 +476,7 @@ If the current MCP capability is unsupported, the workflow is awkward, or the us
477
476
 
478
477
  code_block_tools.register(server)
479
478
  task_context_tools.register(server)
480
- directory_tools.register(server)
479
+ directory_tools.register_frontend_search(server)
481
480
 
482
481
  return server
483
482