@josephyan/qingflow-app-builder-mcp 1.0.9 → 1.1.2

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 (65) hide show
  1. package/README.md +4 -2
  2. package/npm/bin/qingflow-app-builder-mcp.mjs +33 -2
  3. package/npm/lib/runtime.mjs +386 -15
  4. package/npm/scripts/postinstall.mjs +7 -2
  5. package/package.json +1 -1
  6. package/pyproject.toml +2 -1
  7. package/skills/qingflow-app-builder/SKILL.md +135 -14
  8. package/skills/qingflow-app-builder/references/build-complete-system.md +428 -0
  9. package/skills/qingflow-app-builder/references/build-single-app.md +530 -0
  10. package/skills/qingflow-app-builder/references/create-app.md +43 -3
  11. package/skills/qingflow-app-builder/references/gotchas.md +47 -3
  12. package/skills/qingflow-app-builder/references/match-rules.md +114 -0
  13. package/skills/qingflow-app-builder/references/public-surface-sync.md +75 -0
  14. package/skills/qingflow-app-builder/references/tool-selection.md +25 -10
  15. package/skills/qingflow-app-builder/references/update-flow.md +29 -1
  16. package/skills/qingflow-app-builder/references/update-schema.md +6 -2
  17. package/skills/qingflow-app-builder/references/update-views.md +135 -6
  18. package/skills/qingflow-mcp-setup/SKILL.md +111 -0
  19. package/skills/qingflow-mcp-setup/agents/openai.yaml +4 -0
  20. package/skills/qingflow-mcp-setup/references/claude-desktop.md +34 -0
  21. package/skills/qingflow-mcp-setup/references/environments.md +61 -0
  22. package/skills/qingflow-mcp-setup/references/generic-stdio.md +31 -0
  23. package/skills/qingflow-mcp-setup/scripts/check_local_server.sh +38 -0
  24. package/skills/qingflow-workflow-builder/SKILL.md +96 -0
  25. package/skills/qingflow-workflow-builder/manifest.yaml +8 -0
  26. package/skills/qingflow-workflow-builder/references/01-overview.md +45 -0
  27. package/skills/qingflow-workflow-builder/references/02-update-mode.md +53 -0
  28. package/skills/qingflow-workflow-builder/references/03-flow-patterns.md +57 -0
  29. package/skills/qingflow-workflow-builder/references/04-stage1-business-modeling.md +131 -0
  30. package/skills/qingflow-workflow-builder/references/05-stage2-members-roles.md +29 -0
  31. package/skills/qingflow-workflow-builder/references/06-stage3-build-spec.md +165 -0
  32. package/skills/qingflow-workflow-builder/references/07-stage4-validate-spec.md +33 -0
  33. package/skills/qingflow-workflow-builder/references/08-stage5-apply-verify.md +51 -0
  34. package/skills/qingflow-workflow-builder/references/09-stage6-summary.md +88 -0
  35. package/skills/qingflow-workflow-builder/references/10-node-config-reference.md +93 -0
  36. package/skills/qingflow-workflow-builder/references/11-troubleshooting.md +15 -0
  37. package/skills/qingflow-workflow-builder/scripts/diff_flow_spec.py +275 -0
  38. package/skills/qingflow-workflow-builder/scripts/validate_flow_spec.py +605 -0
  39. package/src/qingflow_mcp/__init__.py +1 -1
  40. package/src/qingflow_mcp/backend_client.py +55 -1
  41. package/src/qingflow_mcp/builder_facade/models.py +532 -48
  42. package/src/qingflow_mcp/builder_facade/service.py +9194 -2384
  43. package/src/qingflow_mcp/builder_facade/workflow_spec.py +111 -0
  44. package/src/qingflow_mcp/cli/commands/app.py +3 -16
  45. package/src/qingflow_mcp/cli/commands/builder.py +354 -56
  46. package/src/qingflow_mcp/cli/commands/record.py +112 -6
  47. package/src/qingflow_mcp/cli/formatters.py +102 -2
  48. package/src/qingflow_mcp/cli/main.py +245 -3
  49. package/src/qingflow_mcp/public_surface.py +12 -9
  50. package/src/qingflow_mcp/response_trim.py +287 -22
  51. package/src/qingflow_mcp/server.py +29 -20
  52. package/src/qingflow_mcp/server_app_builder.py +108 -30
  53. package/src/qingflow_mcp/server_app_user.py +28 -23
  54. package/src/qingflow_mcp/solution/compiler/__init__.py +1 -3
  55. package/src/qingflow_mcp/solution/compiler/icon_utils.py +294 -0
  56. package/src/qingflow_mcp/solution/executor.py +3 -133
  57. package/src/qingflow_mcp/tools/ai_builder_tools.py +2617 -440
  58. package/src/qingflow_mcp/tools/app_tools.py +53 -8
  59. package/src/qingflow_mcp/tools/package_tools.py +16 -2
  60. package/src/qingflow_mcp/tools/record_tools.py +14283 -9162
  61. package/src/qingflow_mcp/tools/resource_read_tools.py +3 -0
  62. package/src/qingflow_mcp/tools/solution_tools.py +30 -2
  63. package/src/qingflow_mcp/tools/workflow_tools.py +3 -31
  64. package/src/qingflow_mcp/version.py +110 -0
  65. package/src/qingflow_mcp/solution/compiler/workflow_compiler.py +0 -173
@@ -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),
@@ -78,12 +77,13 @@ USER_PUBLIC_TOOL_SPECS: tuple[PublicToolSpec, ...] = (
78
77
  ("record_code_block_schema_get_public",),
79
78
  ("record", "schema", "code-block"),
80
79
  ),
81
- PublicToolSpec(USER_DOMAIN, "record_member_candidates", ("record_member_candidates",), cli_public=False),
82
- PublicToolSpec(USER_DOMAIN, "record_department_candidates", ("record_department_candidates",), cli_public=False),
83
- PublicToolSpec(USER_DOMAIN, "record_analyze", ("record_analyze",), ("record", "analyze")),
80
+ PublicToolSpec(USER_DOMAIN, "record_member_candidates", ("record_member_candidates",), ("record", "member-candidates")),
81
+ PublicToolSpec(USER_DOMAIN, "record_department_candidates", ("record_department_candidates",), ("record", "department-candidates")),
82
+ PublicToolSpec(USER_DOMAIN, "record_analyze", ("record_analyze",), ("record", "analyze"), mcp_public=False),
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),
@@ -136,11 +138,10 @@ BUILDER_PUBLIC_TOOL_SPECS: tuple[PublicToolSpec, ...] = (
136
138
  PublicToolSpec(BUILDER_DOMAIN, "app_release_edit_lock_if_mine", ("app_release_edit_lock_if_mine",), ("builder", "app", "release-edit-lock-if-mine"), has_contract=True, cli_show_effective_context=True, cli_context_write=True),
137
139
  PublicToolSpec(BUILDER_DOMAIN, "app_resolve", ("app_resolve",), ("builder", "app", "resolve"), has_contract=True, cli_show_effective_context=True),
138
140
  PublicToolSpec(BUILDER_DOMAIN, "button_style_catalog_get", ("button_style_catalog_get",), ("builder", "button", "catalog"), has_contract=True, cli_show_effective_context=True),
139
- PublicToolSpec(BUILDER_DOMAIN, "app_custom_button_list", ("app_custom_button_list",), ("builder", "button", "list"), has_contract=True, cli_show_effective_context=True),
140
- PublicToolSpec(BUILDER_DOMAIN, "app_custom_button_get", ("app_custom_button_get",), ("builder", "button", "get"), has_contract=True, cli_show_effective_context=True),
141
- PublicToolSpec(BUILDER_DOMAIN, "app_custom_button_create", ("app_custom_button_create",), ("builder", "button", "create"), has_contract=True, cli_show_effective_context=True, cli_context_write=True),
142
- PublicToolSpec(BUILDER_DOMAIN, "app_custom_button_update", ("app_custom_button_update",), ("builder", "button", "update"), has_contract=True, cli_show_effective_context=True, cli_context_write=True),
143
- PublicToolSpec(BUILDER_DOMAIN, "app_custom_button_delete", ("app_custom_button_delete",), ("builder", "button", "delete"), has_contract=True, cli_show_effective_context=True, cli_context_write=True),
141
+ PublicToolSpec(BUILDER_DOMAIN, "app_get_buttons", ("app_get_buttons",), ("builder", "button", "get"), has_contract=True, cli_show_effective_context=True),
142
+ PublicToolSpec(BUILDER_DOMAIN, "app_custom_buttons_apply", ("app_custom_buttons_apply",), ("builder", "button", "apply"), has_contract=True, cli_show_effective_context=True, cli_context_write=True),
143
+ PublicToolSpec(BUILDER_DOMAIN, "app_get_associated_resources", ("app_get_associated_resources",), ("builder", "associated-resource", "get"), has_contract=True, cli_show_effective_context=True),
144
+ PublicToolSpec(BUILDER_DOMAIN, "app_associated_resources_apply", ("app_associated_resources_apply",), ("builder", "associated-resource", "apply"), has_contract=True, cli_show_effective_context=True, cli_context_write=True),
144
145
  PublicToolSpec(BUILDER_DOMAIN, "app_get", ("app_get",), ("builder", "app", "get", "summary"), has_contract=True, cli_show_effective_context=True),
145
146
  PublicToolSpec(BUILDER_DOMAIN, "app_get_fields", ("app_get_fields",), ("builder", "app", "get", "fields"), has_contract=True, cli_show_effective_context=True),
146
147
  PublicToolSpec(BUILDER_DOMAIN, "app_repair_code_blocks", ("app_repair_code_blocks",), ("builder", "app", "repair-code-blocks"), has_contract=True, cli_show_effective_context=True),
@@ -154,6 +155,8 @@ BUILDER_PUBLIC_TOOL_SPECS: tuple[PublicToolSpec, ...] = (
154
155
  PublicToolSpec(BUILDER_DOMAIN, "chart_get", ("chart_get",), ("builder", "chart", "get"), has_contract=True, cli_show_effective_context=True),
155
156
  PublicToolSpec(BUILDER_DOMAIN, "app_schema_apply", ("app_schema_apply",), ("builder", "schema", "apply"), has_contract=True, cli_show_effective_context=True, cli_context_write=True),
156
157
  PublicToolSpec(BUILDER_DOMAIN, "app_layout_apply", ("app_layout_apply",), ("builder", "layout", "apply"), has_contract=True, cli_show_effective_context=True, cli_context_write=True),
158
+ PublicToolSpec(BUILDER_DOMAIN, "app_flow_get_schema", ("app_flow_get_schema",), ("builder", "flow", "schema"), has_contract=True, cli_show_effective_context=True),
159
+ PublicToolSpec(BUILDER_DOMAIN, "app_flow_get", ("app_flow_get",), ("builder", "flow", "get"), has_contract=True, cli_show_effective_context=True),
157
160
  PublicToolSpec(BUILDER_DOMAIN, "app_flow_apply", ("app_flow_apply",), ("builder", "flow", "apply"), has_contract=True, cli_show_effective_context=True, cli_context_write=True),
158
161
  PublicToolSpec(BUILDER_DOMAIN, "app_views_apply", ("app_views_apply",), ("builder", "views", "apply"), has_contract=True, cli_show_effective_context=True, cli_context_write=True),
159
162
  PublicToolSpec(BUILDER_DOMAIN, "app_charts_apply", ("app_charts_apply",), ("builder", "charts", "apply"), has_contract=True, cli_show_effective_context=True, cli_context_write=True),
@@ -57,6 +57,13 @@ def trim_public_response(tool_name: str | None, payload: dict[str, Any]) -> dict
57
57
  if not isinstance(payload, dict):
58
58
  return payload
59
59
  if _looks_like_failure_payload(payload):
60
+ 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
+ }:
66
+ return trim_success_response(tool_name, payload)
60
67
  return _trim_returned_failure(payload)
61
68
  return trim_success_response(tool_name, payload)
62
69
 
@@ -65,7 +72,12 @@ def trim_success_response(tool_name: str | None, payload: dict[str, Any]) -> dic
65
72
  if not isinstance(payload, dict):
66
73
  return payload
67
74
  trimmed = deepcopy(payload)
68
- _drop_top_keys(trimmed, COMMON_SUCCESS_DROP_TOP)
75
+ drop_keys = COMMON_SUCCESS_DROP_TOP
76
+ if tool_name in {"user:record_get", "user:record_logs_get"}:
77
+ drop_keys = COMMON_SUCCESS_DROP_TOP - {"output_profile"}
78
+ if tool_name in {"user:record_insert", "user:record_update", "user:record_delete"} and payload.get("ok") is False:
79
+ drop_keys = drop_keys - {"ok"}
80
+ _drop_top_keys(trimmed, drop_keys)
69
81
  transformer = SUCCESS_POLICY_BY_TOOL.get(tool_name or "")
70
82
  if transformer is not None:
71
83
  transformer(trimmed)
@@ -81,7 +93,12 @@ def trim_error_response(payload: dict[str, Any]) -> dict[str, Any]:
81
93
  _drop_deep_keys(trimmed.get("details"), {"request_route", "base_url", "normalized_args", "suggested_next_call", "transport", "response", "body", "raw"})
82
94
  details = trimmed.get("details")
83
95
  if isinstance(details, dict):
96
+ preserved = {}
97
+ for key in ("blocking_issues", "compiled_match_rules"):
98
+ if key in details:
99
+ preserved[key] = details.get(key)
84
100
  compact_details = _compact_scalar_dict(details)
101
+ compact_details.update(preserved)
85
102
  if compact_details:
86
103
  trimmed["details"] = compact_details
87
104
  else:
@@ -137,7 +154,7 @@ def _looks_like_failure_payload(payload: dict[str, Any]) -> bool:
137
154
  if payload.get("ok") is False:
138
155
  return True
139
156
  status = str(payload.get("status") or "").lower()
140
- return status in {"failed", "blocked", "verification_failed"}
157
+ return status in {"failed", "blocked"}
141
158
 
142
159
 
143
160
  def _trim_returned_failure(payload: dict[str, Any]) -> dict[str, Any]:
@@ -146,7 +163,12 @@ def _trim_returned_failure(payload: dict[str, Any]) -> dict[str, Any]:
146
163
  _drop_deep_keys(trimmed.get("details"), {"request_route", "base_url", "normalized_args", "suggested_next_call", "transport", "response", "body", "raw"})
147
164
  details = trimmed.get("details")
148
165
  if isinstance(details, dict):
166
+ preserved = {}
167
+ for key in ("blocking_issues", "compiled_match_rules"):
168
+ if key in details:
169
+ preserved[key] = details.get(key)
149
170
  compact_details = _compact_scalar_dict(details)
171
+ compact_details.update(preserved)
150
172
  if compact_details:
151
173
  trimmed["details"] = compact_details
152
174
  else:
@@ -272,7 +294,7 @@ def _trim_workspace_get(payload: JSONObject) -> None:
272
294
  )
273
295
 
274
296
 
275
- def _trim_app_search_like(payload: JSONObject) -> None:
297
+ def _trim_app_list_like(payload: JSONObject) -> None:
276
298
  payload.pop("apps", None)
277
299
  _trim_item_list(payload, "items", allowed=("app_key", "app_name", "package_name"))
278
300
 
@@ -365,10 +387,17 @@ def _trim_record_write(payload: JSONObject) -> None:
365
387
  data = payload.get("data")
366
388
  if not isinstance(data, dict):
367
389
  return
390
+ if payload.get("mode") == "batch" or data.get("mode") == "batch":
391
+ _trim_record_write_batch(payload, data)
392
+ return
368
393
  data.pop("debug", None)
369
394
  data.pop("normalized_payload", None)
370
395
  data.pop("human_review", None)
371
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
372
401
  resource = _compact_record_resource(data.get("resource"))
373
402
  if resource:
374
403
  data["resource"] = resource
@@ -394,7 +423,48 @@ def _trim_record_write(payload: JSONObject) -> None:
394
423
  data.pop(key, None)
395
424
 
396
425
 
426
+ def _trim_record_write_batch(payload: JSONObject, data: JSONObject) -> None:
427
+ data.pop("items", None)
428
+ data.pop("debug", None)
429
+ for key in ("summary", "app_key", "mode"):
430
+ if data.get(key) in (None, [], {}, ""):
431
+ data.pop(key, None)
432
+ items = payload.get("items")
433
+ if isinstance(items, list):
434
+ payload["items"] = [
435
+ _pick(
436
+ item,
437
+ (
438
+ "index",
439
+ "row_number",
440
+ "status",
441
+ "record_id",
442
+ "apply_id",
443
+ "write_executed",
444
+ "verification_status",
445
+ "safe_to_retry",
446
+ "update_route",
447
+ "tried_routes",
448
+ "failed_fields",
449
+ "confirmation_requests",
450
+ "blockers",
451
+ "error",
452
+ "warnings",
453
+ "resource",
454
+ "verification",
455
+ ),
456
+ )
457
+ for item in items
458
+ if isinstance(item, dict)
459
+ ]
460
+ if payload.get("items") in (None, [], {}, ""):
461
+ payload.pop("items", None)
462
+
463
+
397
464
  def _trim_record_get(payload: JSONObject) -> None:
465
+ if payload.get("fields") is not None or payload.get("semantic_context") is not None:
466
+ _trim_detail_context_record_get(payload)
467
+ return
398
468
  data = payload.get("data")
399
469
  if not isinstance(data, dict):
400
470
  return
@@ -417,22 +487,142 @@ def _trim_record_get(payload: JSONObject) -> None:
417
487
  payload["data"] = compact
418
488
 
419
489
 
490
+ def _trim_detail_context_record_get(payload: JSONObject) -> None:
491
+ _trim_item_list(
492
+ payload,
493
+ "fields",
494
+ allowed=("field_id", "title", "type", "value", "display_value", "requested_focus", "asset_ids", "file_asset_ids"),
495
+ )
496
+ references = payload.get("references")
497
+ if isinstance(references, list):
498
+ compact_refs: list[JSONObject] = []
499
+ for item in references:
500
+ if not isinstance(item, dict):
501
+ continue
502
+ compact = _pick(
503
+ item,
504
+ (
505
+ "field_id",
506
+ "field_title",
507
+ "target_app_key",
508
+ "target_record_id",
509
+ "target_title",
510
+ "target_detail_completeness",
511
+ "self_reference",
512
+ ),
513
+ )
514
+ target_fields = item.get("target_fields") if isinstance(item.get("target_fields"), list) else []
515
+ if target_fields:
516
+ compact["target_fields"] = [
517
+ _pick(field, ("field_id", "title", "type", "value", "display_value", "asset_ids", "file_asset_ids"))
518
+ for field in target_fields
519
+ if isinstance(field, dict)
520
+ ]
521
+ compact_refs.append(compact)
522
+ payload["references"] = compact_refs
523
+ media_assets = payload.get("media_assets")
524
+ if isinstance(media_assets, dict):
525
+ compact_media = _pick(media_assets, ("status", "local_dir", "warnings"))
526
+ items = media_assets.get("items") if isinstance(media_assets.get("items"), list) else []
527
+ compact_media["items"] = [
528
+ _pick(
529
+ item,
530
+ (
531
+ "asset_id",
532
+ "kind",
533
+ "source",
534
+ "field_id",
535
+ "field_title",
536
+ "local_path",
537
+ "access_status",
538
+ "readable_by_agent",
539
+ "mime_type",
540
+ "size_bytes",
541
+ "download_strategy",
542
+ "storage_auth_type",
543
+ "storage_cookie_prefix",
544
+ "redirected",
545
+ ),
546
+ )
547
+ for item in items
548
+ if isinstance(item, dict)
549
+ ]
550
+ payload["media_assets"] = compact_media
551
+ file_assets = payload.get("file_assets")
552
+ if isinstance(file_assets, dict):
553
+ compact_files = _pick(file_assets, ("status", "local_dir", "warnings"))
554
+ items = file_assets.get("items") if isinstance(file_assets.get("items"), list) else []
555
+ compact_files["items"] = [
556
+ _pick(
557
+ item,
558
+ (
559
+ "file_asset_id",
560
+ "media_asset_id",
561
+ "kind",
562
+ "source",
563
+ "field_id",
564
+ "field_title",
565
+ "file_name",
566
+ "local_path",
567
+ "access_status",
568
+ "readable_by_agent",
569
+ "mime_type",
570
+ "size_bytes",
571
+ "download_strategy",
572
+ "storage_auth_type",
573
+ "storage_cookie_prefix",
574
+ "redirected",
575
+ "extraction",
576
+ ),
577
+ )
578
+ for item in items
579
+ if isinstance(item, dict)
580
+ ]
581
+ payload["file_assets"] = compact_files
582
+ for key in ("data_logs", "workflow_logs"):
583
+ node = payload.get(key)
584
+ if not isinstance(node, dict):
585
+ continue
586
+ compact_node = _pick(
587
+ node,
588
+ (
589
+ "status",
590
+ "visible",
591
+ "reason",
592
+ "page",
593
+ "page_size",
594
+ "items_loaded",
595
+ "reported_total",
596
+ "has_more",
597
+ "complete",
598
+ "items",
599
+ ),
600
+ )
601
+ payload[key] = compact_node
602
+ associated_resources = payload.get("associated_resources")
603
+ if isinstance(associated_resources, list):
604
+ payload["associated_resources"] = [
605
+ _pick(item, ("type", "resource_type", "name", "app_key", "app_name", "view_key", "chart_key", "view_type", "report_source", "data_access"))
606
+ for item in associated_resources
607
+ if isinstance(item, dict)
608
+ ]
609
+ _drop_deep_keys(payload, {"raw", "debug"})
610
+
611
+
420
612
  def _trim_record_list(payload: JSONObject) -> None:
421
613
  data = payload.get("data")
422
614
  if not isinstance(data, dict):
423
615
  return
424
616
  pagination = data.get("pagination") if isinstance(data.get("pagination"), dict) else {}
425
- returned_items = pagination.get("returned_items")
426
- result_amount = pagination.get("result_amount")
427
- limit = pagination.get("limit")
428
- truncated = False
429
- if isinstance(result_amount, int) and isinstance(returned_items, int):
617
+ returned_items = pagination.get("returned_count", pagination.get("returned_items"))
618
+ result_amount = pagination.get("total_count", pagination.get("result_amount"))
619
+ truncated = bool(pagination.get("truncated"))
620
+ if not truncated and isinstance(result_amount, int) and isinstance(returned_items, int):
430
621
  truncated = result_amount > returned_items
431
622
  compact_pagination = {
432
623
  "loaded": True,
433
- "page_size": limit,
434
- "fetched_pages": 1,
435
- "reported_total": result_amount,
624
+ "returned_count": returned_items,
625
+ "total_count": result_amount,
436
626
  "truncated": truncated,
437
627
  }
438
628
  selection = data.get("selection") if isinstance(data.get("selection"), dict) else {}
@@ -445,6 +635,17 @@ def _trim_record_list(payload: JSONObject) -> None:
445
635
  if view:
446
636
  compact["view"] = _pick(view, ("view_id", "name"))
447
637
  payload["data"] = compact
638
+ lookup = payload.get("lookup") if isinstance(payload.get("lookup"), dict) else {}
639
+ if lookup:
640
+ payload["lookup"] = {
641
+ "mode": lookup.get("mode"),
642
+ "query": lookup.get("query"),
643
+ "total_count": lookup.get("total_count", lookup.get("reported_total")),
644
+ "returned_count": lookup.get("returned_count"),
645
+ "truncated": lookup.get("truncated"),
646
+ "confidence": lookup.get("confidence"),
647
+ "next_action": lookup.get("next_action"),
648
+ }
448
649
 
449
650
 
450
651
  def _trim_record_access(payload: JSONObject) -> None:
@@ -461,7 +662,6 @@ def _trim_record_access(payload: JSONObject) -> None:
461
662
  "truncated",
462
663
  "safe_for_final_conclusion",
463
664
  "files",
464
- "metadata_files",
465
665
  "fields",
466
666
  "warnings",
467
667
  "verification",
@@ -474,6 +674,49 @@ def _trim_record_access(payload: JSONObject) -> None:
474
674
  payload.update(compact)
475
675
 
476
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
+
477
720
  def _trim_record_analyze(payload: JSONObject) -> None:
478
721
  summary: dict[str, Any] = {}
479
722
  completeness = payload.get("completeness")
@@ -520,13 +763,18 @@ def _trim_record_delete(payload: JSONObject) -> None:
520
763
  if not isinstance(data, dict):
521
764
  return
522
765
  resource = data.get("resource")
523
- deleted_ids: list[str] = []
524
- if isinstance(resource, dict):
766
+ deleted_ids = payload.get("deleted_ids") if isinstance(payload.get("deleted_ids"), list) else data.get("deleted_ids")
767
+ failed_ids = payload.get("failed_ids") if isinstance(payload.get("failed_ids"), list) else data.get("failed_ids")
768
+ if not isinstance(deleted_ids, list):
769
+ deleted_ids = []
770
+ if not isinstance(failed_ids, list):
771
+ failed_ids = []
772
+ if not deleted_ids and isinstance(resource, dict):
525
773
  raw_ids = resource.get("record_ids") or resource.get("apply_ids") or resource.get("applyIds")
526
774
  if isinstance(raw_ids, list):
527
775
  deleted_ids = [str(item) for item in raw_ids if item not in (None, "")]
528
- data["deleted_ids"] = deleted_ids
529
- data.setdefault("failed_ids", [])
776
+ data["deleted_ids"] = [str(item) for item in deleted_ids if item not in (None, "")]
777
+ data["failed_ids"] = [str(item) for item in failed_ids if item not in (None, "")]
530
778
  for key in (
531
779
  "resource",
532
780
  "action",
@@ -594,6 +842,19 @@ def _compact_schema_field(item: Any, *, template_map: dict[str, Any] | None) ->
594
842
  compact["required"] = bool(item.get("required"))
595
843
  if template_map is not None and isinstance(title, str) and title in template_map:
596
844
  compact["template"] = template_map.get(title)
845
+ for key in (
846
+ "format_hint",
847
+ "example_value",
848
+ "linkage",
849
+ "may_become_required",
850
+ "activation_sources",
851
+ "requirement_reason",
852
+ "accepts_natural_input",
853
+ "requires_upload",
854
+ ):
855
+ value = item.get(key)
856
+ if value not in (None, [], {}, ""):
857
+ compact[key] = value
597
858
  candidate_hint = item.get("candidate_hint")
598
859
  if isinstance(candidate_hint, dict):
599
860
  compact["candidate_hint"] = candidate_hint
@@ -747,7 +1008,11 @@ def _trim_builder_envelope(payload: JSONObject) -> None:
747
1008
  details = payload.get("details")
748
1009
  if isinstance(details, dict):
749
1010
  _drop_deep_keys(details, {"request_route", "base_url", "normalized_args", "suggested_next_call", "transport", "response", "body", "raw"})
1011
+ preserved = {}
1012
+ if isinstance(details.get("compiled_match_rules"), dict):
1013
+ preserved["compiled_match_rules"] = details.get("compiled_match_rules")
750
1014
  compact = _compact_scalar_dict(details)
1015
+ compact.update(preserved)
751
1016
  if compact:
752
1017
  payload["details"] = compact
753
1018
  else:
@@ -768,7 +1033,7 @@ _register_policy((USER_DOMAIN, BUILDER_DOMAIN), ("auth_use_credential", "auth_wh
768
1033
  _register_policy((USER_DOMAIN, BUILDER_DOMAIN), ("auth_logout",), _trim_auth_logout)
769
1034
  _register_policy((USER_DOMAIN, BUILDER_DOMAIN), ("workspace_list",), _trim_workspace_list)
770
1035
  _register_policy((USER_DOMAIN, BUILDER_DOMAIN), ("workspace_get", "workspace_select"), _trim_workspace_get)
771
- _register_policy((USER_DOMAIN,), ("app_list", "app_search"), _trim_app_search_like)
1036
+ _register_policy((USER_DOMAIN,), ("app_list",), _trim_app_list_like)
772
1037
  _register_policy((USER_DOMAIN, BUILDER_DOMAIN), ("app_get",), _trim_app_get)
773
1038
  _register_policy((BUILDER_DOMAIN,), ("app_repair_code_blocks",), _trim_builder_list_like)
774
1039
  _register_policy((USER_DOMAIN, BUILDER_DOMAIN), ("portal_list", "portal_get", "view_get", "chart_get"), _trim_builder_list_like)
@@ -811,6 +1076,7 @@ _register_policy((USER_DOMAIN,), ("record_insert", "record_update"), _trim_recor
811
1076
  _register_policy((USER_DOMAIN,), ("record_get",), _trim_record_get)
812
1077
  _register_policy((USER_DOMAIN,), ("record_list",), _trim_record_list)
813
1078
  _register_policy((USER_DOMAIN,), ("record_access",), _trim_record_access)
1079
+ _register_policy((USER_DOMAIN,), ("record_logs_get",), _trim_record_logs)
814
1080
  _register_policy((USER_DOMAIN,), ("record_analyze",), _trim_record_analyze)
815
1081
  _register_policy((USER_DOMAIN,), ("record_code_block_run",), _trim_code_block_run)
816
1082
  _register_policy((USER_DOMAIN,), ("task_list",), _trim_task_list)
@@ -851,6 +1117,8 @@ _register_policy(
851
1117
  (BUILDER_DOMAIN,),
852
1118
  (
853
1119
  "builder_tool_contract",
1120
+ "workspace_icon_catalog_get",
1121
+ "package_list",
854
1122
  "package_get",
855
1123
  "package_apply",
856
1124
  "solution_install",
@@ -860,11 +1128,8 @@ _register_policy(
860
1128
  "app_release_edit_lock_if_mine",
861
1129
  "app_resolve",
862
1130
  "button_style_catalog_get",
863
- "app_custom_button_list",
864
- "app_custom_button_get",
865
- "app_custom_button_create",
866
- "app_custom_button_update",
867
- "app_custom_button_delete",
1131
+ "app_custom_buttons_apply",
1132
+ "app_associated_resources_apply",
868
1133
  "app_get_fields",
869
1134
  "app_repair_code_blocks",
870
1135
  "app_get_layout",
@@ -48,17 +48,17 @@ 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
- If an accessible view has `analysis_supported=false`, do not use it for `record_list` or `record_analyze`. `boardView` and `ganttView` are special UI views, not list/analyze targets.
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.
55
55
 
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_list`, `record_get`, or `record_analyze`.
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,8 +67,9 @@ 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`.
71
- `record_browse_schema_get(view_id=...)` returns browse-schema fields for the selected accessible view.
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
+ `record_browse_schema_get(view_id=...)` returns the same readable fields shown in the selected Qingflow table view header.
72
+ `record_access.fields` / CSV columns and `record_list.columns / where / order_by / query_fields` use that exact same view schema.
72
73
  `record_code_block_schema_get` returns code-block-ready schema for exact code block field selection.
73
74
  `record_import_schema_get` returns import-ready column metadata.
74
75
 
@@ -78,16 +79,17 @@ Call `record_import_schema_get` when the import field mapping is unclear before
78
79
 
79
80
  ## Analytics Path
80
81
 
81
- `app_get -> record_browse_schema_get(view_id=...) -> record_analyze`
82
+ `app_get -> record_browse_schema_get(view_id=...) -> record_access -> Python`
82
83
 
83
84
  Prefer `view_id` entries from `accessible_views` where `analysis_supported=true`.
84
85
 
86
+ Use `record_access` to write local CSV shard files for analysis, then use Python to compute counts, rankings, ratios, trends, and final conclusions.
87
+
85
88
  Use this DSL shape:
86
89
 
87
- - `dimensions`: `{{field_id, alias, bucket}}`
88
- - `metrics`: `{{op, field_id, alias}}`
89
- - `filters`: `{{field_id, op, value}}`
90
- - `sort`: `{{by, order}}`
90
+ - `columns`: `[{{field_id}}]`
91
+ - `where`: `[{{field_id, op, value}}]`
92
+ - `order_by`: `[{{field_id, direction}}]`
91
93
 
92
94
  Important key rules:
93
95
 
@@ -101,30 +103,37 @@ Analysis answers must include concrete numbers. When applicable, include percent
101
103
 
102
104
  ## Record CRUD Path
103
105
 
104
- `app_get -> record_browse_schema_get(view_id=...) -> record_list / record_get`
105
- `record_insert_schema_get -> record_insert`
106
- `record_update_schema_get -> record_update`
106
+ `app_get -> record_browse_schema_get(view_id=...) -> record_list / record_get / record_logs_get`
107
+ `record_insert_schema_get -> record_insert(items)`
108
+ `record_update` for simple updates; `record_update_schema_get -> record_update` when the writable field scope is unclear.
107
109
  `record_list / record_get -> record_delete`
108
110
  `record_code_block_schema_get -> record_code_block_run`
109
111
 
110
112
  - Use `columns` as `[{{field_id}}]`
113
+ - 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.
111
114
  - Use `where` items as `{{field_id, op, value}}`
112
115
  - Use `order_by` items as `{{field_id, direction}}`
113
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
114
117
 
115
- - `record_insert` uses an applicant-node `fields` map keyed by field title.
116
- - `record_update` uses a field-title keyed `fields` map and internally selects the first accessible view that can execute the current payload.
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. 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.
117
120
  - For insert, `runtime_linked_required_fields` means required-but-not-directly-writable fields that are usually supplied by runtime linkage or upstream context.
118
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.
119
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.
120
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.
121
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.
122
- - `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.
123
126
  - `record_delete` deletes by `record_id` or `record_ids`.
124
- - When readback shape matters after insert or update, prefer `record_get(..., output_profile="normalized")` or `record_list(..., output_profile="normalized")`.
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.
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.
130
+ - `record_get.columns` are focus hints only; they do not project the detail fields. Read facts from top-level `fields[]`.
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.
125
132
 
126
133
  - Read relation targets from `record_insert_schema_get` / `record_update_schema_get` relation metadata before preparing relation writes.
127
- - Member and department fields may be written with natural strings directly on `record_insert` / `record_update`; only fall back to `record_member_candidates` or `record_department_candidates` when the user wants explicit candidate browsing or the write returns ambiguity that needs confirmation.
134
+ - Member, department, and relation fields may be written with natural strings directly on `record_insert` / `record_update`; only fall back to candidate tools when the user wants explicit candidate browsing or the write returns ambiguity that needs confirmation.
135
+ - CLI-only agents can use `qingflow record member-candidates --app-key APP_KEY --field-id FIELD_ID --keyword NAME --json` or `qingflow record department-candidates --app-key APP_KEY --field-id FIELD_ID --keyword DEPT --json` for that fallback; pass `--record-id`, `--workflow-node-id`, and `--fields-file` only when the candidate scope must match an existing runtime context.
136
+ - For batch insert `partial_success`, read `created_record_ids`, failed `items[].row_number`, and `failed_fields`; repair only failed rows and never retry the whole batch after any row has `write_executed=true`.
128
137
  - If explicit candidate browsing is needed for default-all member or department fields, prefer those field candidate tools instead of starting with `directory_*`.
129
138
 
130
139
  ## Code Block Path
@@ -177,7 +186,7 @@ Use `record_code_block_run` when the user wants to execute a form code-block fie
177
186
  - `task_action_execute(task_id=..., action=...)` is also supported; MCP resolves the current todo locator internally before calling the real action route.
178
187
  - `task_workflow_log_get(task_id=...)` and `task_associated_report_detail_get(task_id=...)` are also supported for the current todo context.
179
188
  - Use `task_associated_report_detail_get` for associated view or report details.
180
- - 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?)`.
181
190
  - Task actions operate on `app_key + record_id + workflow_node_id`, not `task_id`.
182
191
 
183
192
  ## Time Handling