@qingflow-tech/qingflow-cli 1.1.43 → 1.1.44-beta.1

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 (77) hide show
  1. package/README.md +13 -6
  2. package/entry_point.py +1 -1
  3. package/npm/bin/qingflow.mjs +1 -2
  4. package/npm/lib/runtime.mjs +3 -7
  5. package/package.json +2 -2
  6. package/pyproject.toml +3 -4
  7. package/skills/qingflow-app-form/SKILL.md +66 -0
  8. package/skills/qingflow-app-form/agents/openai.yaml +4 -0
  9. package/skills/qingflow-cli/SKILL.md +8 -6
  10. package/skills/qingflow-cli/manifest.yaml +1 -1
  11. package/skills/qingflow-cli/reference/QINGFLOW_CLI_BUILDER_APP_DELIVERY_WORKFLOW.md +4 -4
  12. package/skills/qingflow-cli/reference/QINGFLOW_CLI_EXPLORATION_REPORT.md +1 -1
  13. package/skills/qingflow-cli/reference/QINGFLOW_CLI_FIELD_DATA_TYPES.md +1 -1
  14. package/skills/qingflow-cli/reference/QINGFLOW_CLI_SCHEMA_APPLY_FIELD_TYPES_AND_SCENARIOS.md +4 -4
  15. package/skills/qingflow-cli/reference/builder/10-build-single-app.md +5 -5
  16. package/skills/qingflow-cli/reference/builder/20-build-complete-system.md +14 -14
  17. package/skills/qingflow-cli/reference/builder/30-schema-fields.md +106 -103
  18. package/skills/qingflow-cli/reference/builder/40-layout.md +39 -31
  19. package/skills/qingflow-cli/reference/builder/README.md +9 -9
  20. package/skills/qingflow-cli/reference/builder/code-integrations/README.md +4 -2
  21. package/skills/qingflow-cli/reference/builder/code-integrations/code-block.md +2 -0
  22. package/skills/qingflow-cli/reference/builder/code-integrations/q-linker.md +2 -0
  23. package/skills/qingflow-cli/reference/builder/reference/app-delivery-sop.md +45 -38
  24. package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_EXPLORATION_REPORT.md +1 -1
  25. package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_FIELD_DATA_TYPES.md +1 -1
  26. package/skills/qingflow-cli/reference/examples/README.md +3 -3
  27. package/skills/qingflow-common/SKILL.md +47 -0
  28. package/skills/qingflow-common/agents/openai.yaml +4 -0
  29. package/src/qingflow_mcp/__main__.py +2 -3
  30. package/src/qingflow_mcp/app_form_compat.py +104 -0
  31. package/src/qingflow_mcp/app_form_validator.py +462 -0
  32. package/src/qingflow_mcp/builder_facade/app_form.py +4196 -0
  33. package/src/qingflow_mcp/builder_facade/app_form_schema.py +425 -0
  34. package/src/qingflow_mcp/builder_facade/schemas/app_form/manifest.json +42 -0
  35. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/complete.json +2969 -0
  36. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/address.json +392 -0
  37. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/amount.json +236 -0
  38. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/attachment.json +425 -0
  39. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/code_block.json +318 -0
  40. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/data_relation.json +580 -0
  41. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/date.json +150 -0
  42. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/datetime.json +150 -0
  43. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/department.json +345 -0
  44. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/email.json +190 -0
  45. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/link.json +190 -0
  46. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/long_text.json +190 -0
  47. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/member.json +484 -0
  48. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/multi_select.json +214 -0
  49. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/number.json +236 -0
  50. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/phone.json +199 -0
  51. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/q_linker.json +412 -0
  52. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/relation.json +454 -0
  53. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/single_choice.json +214 -0
  54. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/single_select.json +214 -0
  55. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/subtable.json +1921 -0
  56. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/text.json +190 -0
  57. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/formula.json +1594 -0
  58. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/summary.json +718 -0
  59. package/src/qingflow_mcp/builder_facade/service.py +8 -4
  60. package/src/qingflow_mcp/cli/commands/builder.py +93 -28
  61. package/src/qingflow_mcp/cli/json_io.py +21 -2
  62. package/src/qingflow_mcp/cli/main.py +3 -39
  63. package/src/qingflow_mcp/public_surface.py +5 -2
  64. package/src/qingflow_mcp/response_trim.py +50 -0
  65. package/src/qingflow_mcp/server.py +24 -3
  66. package/src/qingflow_mcp/server_app_builder.py +44 -87
  67. package/src/qingflow_mcp/tools/ai_builder_tools.py +103 -100
  68. package/src/qingflow_mcp/tools/app_tools.py +46 -65
  69. package/src/qingflow_mcp/tools/record_tools.py +3 -3
  70. package/src/qingflow_mcp/version.py +2 -4
  71. package/docs/local-agent-install.md +0 -370
  72. package/skills/qingflow-mcp-setup/SKILL.md +0 -115
  73. package/skills/qingflow-mcp-setup/agents/openai.yaml +0 -4
  74. package/skills/qingflow-mcp-setup/references/claude-desktop.md +0 -34
  75. package/skills/qingflow-mcp-setup/references/environments.md +0 -62
  76. package/skills/qingflow-mcp-setup/references/generic-stdio.md +0 -32
  77. package/skills/qingflow-mcp-setup/scripts/check_local_server.sh +0 -38
@@ -9191,6 +9191,7 @@ class AiBuilderFacade:
9191
9191
  remove_fields: list[FieldRemovePatch],
9192
9192
  inline_form_layout_sections: list[LayoutSectionPatch] | None = None,
9193
9193
  ) -> JSONObject:
9194
+ # TODO(deprecation): Remove after all internal callers use the AppForm reconciler.
9194
9195
  apply_started_at = time.perf_counter()
9195
9196
  schema_write_ms: int | None = None
9196
9197
  inline_layout_ms: int | None = None
@@ -10182,6 +10183,7 @@ class AiBuilderFacade:
10182
10183
  sections: list[LayoutSectionPatch],
10183
10184
  publish: bool = True,
10184
10185
  ) -> JSONObject:
10186
+ # TODO(deprecation): Remove after all internal callers use the AppForm reconciler.
10185
10187
  requested_sections = [section.model_dump(mode="json", exclude_none=True) for section in sections]
10186
10188
  normalized_args = {
10187
10189
  "app_key": app_key,
@@ -22236,7 +22238,7 @@ def _compile_code_block_binding_fields(
22236
22238
  alias_name = str(output_item.get("alias") or "").strip()
22237
22239
  alias_path = str(output_item.get("path") or "").strip()
22238
22240
  existing_alias = existing_alias_by_name.get(alias_name) or {}
22239
- alias_id = _coerce_positive_int(existing_alias.get("alias_id"))
22241
+ alias_id = _coerce_positive_int(output_item.get("alias_id")) or _coerce_positive_int(existing_alias.get("alias_id"))
22240
22242
  compiled_outputs.append(
22241
22243
  {
22242
22244
  "alias_name": alias_name,
@@ -22649,7 +22651,8 @@ def _compile_q_linker_binding_fields(
22649
22651
  {
22650
22652
  "alias_name": alias_name,
22651
22653
  "alias_path": alias_path,
22652
- "alias_id": _coerce_positive_int((existing_alias_by_name.get(alias_name) or {}).get("alias_id")),
22654
+ "alias_id": _coerce_positive_int(output_item.get("alias_id"))
22655
+ or _coerce_positive_int((existing_alias_by_name.get(alias_name) or {}).get("alias_id")),
22653
22656
  "target_field": {
22654
22657
  "field_id": target_field.get("field_id"),
22655
22658
  "que_id": target_ref,
@@ -22715,6 +22718,7 @@ def _compile_q_linker_binding_fields(
22715
22718
  "target_field_name": target_field.get("name"),
22716
22719
  "target_field_ref": target_ref,
22717
22720
  "alias": output_item.get("alias_name"),
22721
+ "alias_id": _coerce_positive_int(output_item.get("alias_id")),
22718
22722
  }
22719
22723
  )
22720
22724
 
@@ -22787,7 +22791,7 @@ def _materialize_q_linker_question_relations(
22787
22791
  "queId": source_ref,
22788
22792
  "queTitle": source_field.get("name"),
22789
22793
  "qlinkerAlias": relation.get("alias"),
22790
- "aliasId": None,
22794
+ "aliasId": _coerce_positive_int(relation.get("alias_id")),
22791
22795
  },
22792
22796
  "matchRuleType": 1,
22793
22797
  "matchRules": [],
@@ -24004,7 +24008,7 @@ def _normalize_formula_defaults_for_save(value: Any) -> None:
24004
24008
  return
24005
24009
  if not isinstance(value, dict):
24006
24010
  return
24007
- if _coerce_any_int(value.get("queDefaultType")) == DEFAULT_TYPE_FORMULA and value.get("queDefaultValue"):
24011
+ if value.get("queDefaultValue"):
24008
24012
  value["queDefaultValue"] = _encode_formula_for_backend_save(value.get("queDefaultValue"))
24009
24013
  for key in ("subQuestions", "innerQuestions"):
24010
24014
  nested = value.get(key)
@@ -4,6 +4,7 @@ import argparse
4
4
  from copy import deepcopy
5
5
 
6
6
  from ..context import CliContext
7
+ from ...app_form_validator import AppFormValidationError, validate_declaration
7
8
  from ..json_io import load_json_value
8
9
  from .common import load_list_arg, load_object_arg, parse_bool_text, raise_config_error, require_list_arg
9
10
 
@@ -98,6 +99,30 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
98
99
  package_apply.add_argument("--config-file", required=True)
99
100
  package_apply.set_defaults(handler=_handle_package_apply, format_hint="builder_summary", force_json_output=True)
100
101
 
102
+ app_form = builder_subparsers.add_parser("app-form", help="声明式应用表单")
103
+ app_form_subparsers = app_form.add_subparsers(dest="builder_app_form_command", required=True)
104
+
105
+ app_form_schema = app_form_subparsers.add_parser("schema", help="读取 AppForm Schema")
106
+ app_form_schema.add_argument("--schema-version")
107
+ app_form_schema.add_argument("--schema-kind", choices=("summary", "field", "formula"))
108
+ app_form_schema.add_argument("--field-type")
109
+ app_form_schema.set_defaults(handler=_handle_app_form_schema, format_hint="builder_summary", force_json_output=True)
110
+
111
+ app_form_get = app_form_subparsers.add_parser("get", help="读取 canonical AppForm")
112
+ app_form_get.add_argument("--app-key", required=True)
113
+ app_form_get.add_argument("--schema-version")
114
+ app_form_get.add_argument("--being-draft", action=argparse.BooleanOptionalAction, default=False)
115
+ app_form_get.set_defaults(handler=_handle_app_form_get, format_hint="builder_summary", force_json_output=True)
116
+
117
+ app_form_apply = app_form_subparsers.add_parser("apply", help="应用完整 AppForm 声明")
118
+ app_form_apply.add_argument("--file", required=True, help="AppForm JSON 文件,- 表示 stdin")
119
+ app_form_apply.set_defaults(handler=_handle_app_form_apply, format_hint="builder_summary", force_json_output=True)
120
+
121
+ app_form_validate = app_form_subparsers.add_parser("validate", help="只校验 AppForm 声明,不访问后端")
122
+ app_form_validate.add_argument("--file", required=True, help="AppForm JSON 文件,- 表示 stdin")
123
+ app_form_validate.add_argument("--schema-version", help="固定使用的 AppForm Schema 版本")
124
+ app_form_validate.set_defaults(handler=_handle_app_form_validate, format_hint="builder_summary", force_json_output=True)
125
+
101
126
  app = builder_subparsers.add_parser("app", help="应用")
102
127
  app_subparsers = app.add_subparsers(dest="builder_app_command", required=True)
103
128
 
@@ -113,6 +138,10 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
113
138
  app_release_lock.add_argument("--lock-owner-name", required=True)
114
139
  app_release_lock.set_defaults(handler=_handle_app_release_edit_lock_if_mine, format_hint="builder_summary", force_json_output=True)
115
140
 
141
+ app_delete = app_subparsers.add_parser("delete", help="删除应用")
142
+ app_delete.add_argument("--app-key", required=True)
143
+ app_delete.set_defaults(handler=_handle_app_delete, format_hint="builder_summary", force_json_output=True)
144
+
116
145
  app_get = app_subparsers.add_parser("get", help="读取应用配置(字段请使用: builder app get --app-key APP fields)")
117
146
  app_get.add_argument(
118
147
  "builder_app_get_section",
@@ -202,34 +231,6 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
202
231
  portal_delete.add_argument("--dash-key", required=True)
203
232
  portal_delete.set_defaults(handler=_handle_portal_delete, format_hint="builder_summary", force_json_output=True)
204
233
 
205
- schema_apply = builder_subparsers.add_parser("schema", help="字段搭建")
206
- schema_apply_subparsers = schema_apply.add_subparsers(dest="builder_schema_command", required=True)
207
- schema_apply_apply = schema_apply_subparsers.add_parser("apply", help="执行字段变更")
208
- schema_apply_apply.add_argument("--app-key", default="")
209
- schema_apply_apply.add_argument("--package-id", type=int)
210
- schema_apply_apply.add_argument("--app-name", default="")
211
- schema_apply_apply.add_argument("--app-title", default="")
212
- schema_apply_apply.add_argument("--icon")
213
- schema_apply_apply.add_argument("--color")
214
- schema_apply_apply.add_argument("--visibility-file")
215
- schema_apply_apply.add_argument("--publish", action=argparse.BooleanOptionalAction, default=None)
216
- schema_apply_apply.add_argument("--apps-file", help="多应用 schema JSON 对象;每项可带 client_key/app_name/form,支持 relation target_app_ref")
217
- schema_apply_apply.add_argument("--form-file", help="单应用表单结构 JSON 数组;form[].rows[][] 同时声明字段和表单分组布局")
218
- schema_apply_apply.add_argument("--add-fields-file", help="字段 JSON 数组;字段可用 as_data_title/as_data_cover 标记数据标题/封面")
219
- schema_apply_apply.add_argument("--update-fields-file", help="字段更新 JSON 数组;set 内可用 as_data_title/as_data_cover 标记数据标题/封面")
220
- schema_apply_apply.add_argument("--remove-fields-file")
221
- schema_apply_apply.set_defaults(handler=_handle_schema_apply, format_hint="builder_summary", force_json_output=True)
222
-
223
- layout_apply = builder_subparsers.add_parser("layout", help="布局")
224
- layout_apply_subparsers = layout_apply.add_subparsers(dest="builder_layout_command", required=True)
225
- layout_apply_apply = layout_apply_subparsers.add_parser("apply", help="执行布局变更")
226
- layout_apply_apply.add_argument("--app-key", default="")
227
- layout_apply_apply.add_argument("--apps-file", help="多应用布局 JSON 数组;每项包含 app_key/mode?/publish?/sections")
228
- layout_apply_apply.add_argument("--mode", choices=["merge", "replace"], default="merge")
229
- layout_apply_apply.add_argument("--publish", action=argparse.BooleanOptionalAction, default=True)
230
- layout_apply_apply.add_argument("--sections-file")
231
- layout_apply_apply.set_defaults(handler=_handle_layout_apply, format_hint="builder_summary", force_json_output=True)
232
-
233
234
  views_apply = builder_subparsers.add_parser("views", help="视图")
234
235
  views_apply_subparsers = views_apply.add_subparsers(dest="builder_views_command", required=True)
235
236
  views_apply_apply = views_apply_subparsers.add_parser("apply", help="执行视图变更")
@@ -331,6 +332,68 @@ def _handle_contract(args: argparse.Namespace, context: CliContext) -> dict:
331
332
  return context.builder.builder_tool_contract(tool_name=args.tool_name)
332
333
 
333
334
 
335
+ def _handle_app_form_schema(args: argparse.Namespace, context: CliContext) -> dict:
336
+ return context.builder.app_form_schema(
337
+ schema_version=args.schema_version,
338
+ field_type=args.field_type,
339
+ schema_kind=args.schema_kind,
340
+ )
341
+
342
+
343
+ def _handle_app_form_get(args: argparse.Namespace, context: CliContext) -> dict:
344
+ return context.builder.app_form_get(
345
+ profile=args.profile,
346
+ app_key=args.app_key,
347
+ schema_version=args.schema_version,
348
+ being_draft=bool(args.being_draft),
349
+ )
350
+
351
+
352
+ def _handle_app_form_apply(args: argparse.Namespace, context: CliContext) -> dict:
353
+ payload = load_json_value(
354
+ args.file,
355
+ option_name="--file",
356
+ max_bytes=2 * 1024 * 1024,
357
+ max_depth=64,
358
+ )
359
+ if not isinstance(payload, dict):
360
+ raise_config_error("builder app-form apply --file must contain a JSON object")
361
+ return context.builder.app_form_apply(profile=args.profile, spec=payload)
362
+
363
+
364
+ def _handle_app_form_validate(args: argparse.Namespace, _context: CliContext) -> dict:
365
+ payload = load_json_value(
366
+ args.file,
367
+ option_name="--file",
368
+ max_bytes=2 * 1024 * 1024,
369
+ max_depth=64,
370
+ )
371
+ if not isinstance(payload, dict):
372
+ raise_config_error("builder app-form validate --file must contain a JSON object")
373
+ try:
374
+ result = validate_declaration(payload, schema_version=args.schema_version)
375
+ except AppFormValidationError as error:
376
+ return {
377
+ "category": "validation",
378
+ "status": "failed",
379
+ "ok": False,
380
+ "error_code": "INVALID_SPEC",
381
+ "message": "AppForm declaration failed validation",
382
+ "details": {
383
+ "schemaVersion": error.schema_version,
384
+ "apiVersion": error.api_version,
385
+ "issues": error.issues,
386
+ },
387
+ }
388
+ except (ValueError, RuntimeError) as error:
389
+ raise_config_error(str(error), error_code="VALIDATION_ERROR")
390
+ return {"category": "validation", "status": "success", "ok": True, **result, "message": "AppForm declaration is valid"}
391
+
392
+
393
+ def _handle_app_delete(args: argparse.Namespace, context: CliContext) -> dict:
394
+ return context.builder.app_delete(profile=args.profile, app_key=args.app_key)
395
+
396
+
334
397
  def _handle_icon_catalog(args: argparse.Namespace, context: CliContext) -> dict:
335
398
  return context.builder.workspace_icon_catalog_get(profile=args.profile)
336
399
 
@@ -554,6 +617,8 @@ def _handle_chart_get(args: argparse.Namespace, context: CliContext) -> dict:
554
617
  return context.builder.chart_get(profile=args.profile, chart_id=args.chart_id)
555
618
 
556
619
 
620
+ # TODO(deprecation): The legacy schema/layout CLI routes are removed. Keep the implementation below only
621
+ # while internal migration and focused regression coverage still depend on the imperative adapters.
557
622
  def _handle_schema_apply(args: argparse.Namespace, context: CliContext) -> dict:
558
623
  apps_payload = _load_apps_file_arg(args.apps_file)
559
624
  apps = apps_payload["apps"]
@@ -8,7 +8,13 @@ from typing import Any
8
8
  from ..errors import QingflowApiError
9
9
 
10
10
 
11
- def load_json_value(path: str, *, option_name: str) -> Any:
11
+ def load_json_value(
12
+ path: str,
13
+ *,
14
+ option_name: str,
15
+ max_bytes: int | None = None,
16
+ max_depth: int | None = None,
17
+ ) -> Any:
12
18
  if not path:
13
19
  raise QingflowApiError.config_error(f"{option_name} is required")
14
20
  if path == "-":
@@ -18,10 +24,23 @@ def load_json_value(path: str, *, option_name: str) -> Any:
18
24
  raw = Path(path).expanduser().read_text(encoding="utf-8")
19
25
  except OSError as exc:
20
26
  raise QingflowApiError.config_error(f"failed to read {option_name} from '{path}': {exc}") from exc
27
+ if max_bytes is not None and len(raw.encode("utf-8")) > max_bytes:
28
+ raise QingflowApiError.config_error(f"{option_name} exceeds the {max_bytes}-byte limit")
21
29
  try:
22
- return json.loads(raw)
30
+ value = json.loads(raw)
23
31
  except json.JSONDecodeError as exc:
24
32
  raise QingflowApiError.config_error(f"{option_name} must contain valid JSON: {exc}") from exc
33
+ if max_depth is not None:
34
+ stack = [(value, 1)]
35
+ while stack:
36
+ item, depth = stack.pop()
37
+ if depth > max_depth:
38
+ raise QingflowApiError.config_error(f"{option_name} exceeds the maximum nesting depth {max_depth}")
39
+ if isinstance(item, dict):
40
+ stack.extend((child, depth + 1) for child in item.values())
41
+ elif isinstance(item, list):
42
+ stack.extend((child, depth + 1) for child in item)
43
+ return value
25
44
 
26
45
 
27
46
  def load_json_object(path: str, *, option_name: str) -> dict[str, Any]:
@@ -79,23 +79,6 @@ def run(
79
79
  return 2
80
80
  except SystemExit as exc:
81
81
  return int(exc.code or 0)
82
- try:
83
- _validate_conditional_args(args)
84
- except _CliArgumentError as exc:
85
- if _should_force_json_output_argv(normalized_argv):
86
- payload = {
87
- "category": "config",
88
- "status": "failed",
89
- "error_code": "ARGUMENT_ERROR",
90
- "message": exc.message,
91
- "details": {"usage": exc.usage.strip(), "prog": exc.prog},
92
- }
93
- payload = _maybe_attach_builder_apply_error_envelope_from_args(args, payload)
94
- emit_json_result(payload, stream=out)
95
- return 2
96
- err.write(exc.usage)
97
- err.write(f"{exc.prog}: error: {exc.message}\n")
98
- return 2
99
82
  setattr(args, "_stdin", sys.stdin)
100
83
  setattr(args, "_stdout_stream", out)
101
84
  setattr(args, "_stderr_stream", err)
@@ -161,21 +144,6 @@ def _emit_version(*, json_mode: bool, stdout: TextIO) -> int:
161
144
  return 0
162
145
 
163
146
 
164
- def _validate_conditional_args(args: argparse.Namespace) -> None:
165
- if (
166
- getattr(args, "command", "") in {"builder", "build"}
167
- and getattr(args, "builder_command", "") == "layout"
168
- and getattr(args, "builder_layout_command", "") == "apply"
169
- and not getattr(args, "apps_file", None)
170
- and not getattr(args, "sections_file", None)
171
- ):
172
- raise _CliArgumentError(
173
- prog="qingflow builder layout apply",
174
- message="--sections-file is required",
175
- usage="usage: qingflow builder layout apply --app-key APP_KEY --sections-file SECTIONS.json\n",
176
- )
177
-
178
-
179
147
  def _should_force_json_output_argv_for_version(argv: list[str]) -> bool:
180
148
  return "--json" in argv
181
149
 
@@ -231,8 +199,10 @@ def _should_force_json_output_argv(argv: list[str]) -> bool:
231
199
  return False
232
200
  section = tokens[1]
233
201
  action = tokens[2]
234
- if section in {"package", "button", "associated-resource", "associated-resources", "portal", "schema", "layout", "views", "flow", "charts"}:
202
+ if section in {"package", "button", "associated-resource", "associated-resources", "portal", "views", "flow", "charts"}:
235
203
  return action == "apply"
204
+ if section == "app-form":
205
+ return action in {"schema", "get", "validate", "apply"}
236
206
  if section == "publish":
237
207
  return action == "verify"
238
208
  if section == "app":
@@ -283,10 +253,6 @@ def _builder_apply_operation_from_args(args: argparse.Namespace) -> str | None:
283
253
  return "portal_apply"
284
254
  if section == "portal" and getattr(args, "builder_portal_command", "") == "delete":
285
255
  return "portal_delete"
286
- if section == "schema" and getattr(args, "builder_schema_command", "") == "apply":
287
- return "app_schema_apply"
288
- if section == "layout" and getattr(args, "builder_layout_command", "") == "apply":
289
- return "app_layout_apply"
290
256
  if section == "views" and getattr(args, "builder_views_command", "") == "apply":
291
257
  return "app_views_apply"
292
258
  if section == "flow" and getattr(args, "builder_flow_command", "") == "apply":
@@ -312,8 +278,6 @@ def _builder_apply_operation_from_argv(argv: list[str]) -> str | None:
312
278
  "associated-resource": "app_associated_resources_apply",
313
279
  "associated-resources": "app_associated_resources_apply",
314
280
  "portal": "portal_apply",
315
- "schema": "app_schema_apply",
316
- "layout": "app_layout_apply",
317
281
  "views": "app_views_apply",
318
282
  "flow": "app_flow_apply",
319
283
  "charts": "app_charts_apply",
@@ -142,6 +142,11 @@ BUILDER_PUBLIC_TOOL_SPECS: tuple[PublicToolSpec, ...] = (
142
142
  PublicToolSpec(BUILDER_DOMAIN, "role_create", ("role_create",), ("builder", "role", "create"), has_contract=True, cli_show_effective_context=True, cli_context_write=True),
143
143
  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),
144
144
  PublicToolSpec(BUILDER_DOMAIN, "app_resolve", ("app_resolve",), ("builder", "app", "resolve"), has_contract=True, cli_show_effective_context=True),
145
+ PublicToolSpec(BUILDER_DOMAIN, "app_form_schema", ("app_form_schema",), ("builder", "app-form", "schema")),
146
+ PublicToolSpec(BUILDER_DOMAIN, "app_form_validate", (), ("builder", "app-form", "validate"), mcp_public=False),
147
+ PublicToolSpec(BUILDER_DOMAIN, "app_form_get", ("app_form_get",), ("builder", "app-form", "get"), cli_show_effective_context=True),
148
+ PublicToolSpec(BUILDER_DOMAIN, "app_form_apply", ("app_form_apply",), ("builder", "app-form", "apply"), cli_show_effective_context=True, cli_context_write=True),
149
+ PublicToolSpec(BUILDER_DOMAIN, "app_delete", ("app_delete",), ("builder", "app", "delete"), cli_show_effective_context=True, cli_context_write=True),
145
150
  PublicToolSpec(BUILDER_DOMAIN, "button_style_catalog_get", ("button_style_catalog_get",), ("builder", "button", "catalog"), has_contract=True, cli_show_effective_context=True),
146
151
  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),
147
152
  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),
@@ -158,8 +163,6 @@ BUILDER_PUBLIC_TOOL_SPECS: tuple[PublicToolSpec, ...] = (
158
163
  PublicToolSpec(BUILDER_DOMAIN, "portal_get", ("portal_get",), ("builder", "portal", "get"), has_contract=True, cli_show_effective_context=True),
159
164
  PublicToolSpec(BUILDER_DOMAIN, "view_get", ("view_get",), ("builder", "view", "get"), has_contract=True, cli_show_effective_context=True),
160
165
  PublicToolSpec(BUILDER_DOMAIN, "chart_get", ("chart_get",), ("builder", "chart", "get"), has_contract=True, cli_show_effective_context=True),
161
- PublicToolSpec(BUILDER_DOMAIN, "app_schema_apply", ("app_schema_apply",), ("builder", "schema", "apply"), has_contract=True, cli_show_effective_context=True, cli_context_write=True),
162
- PublicToolSpec(BUILDER_DOMAIN, "app_layout_apply", ("app_layout_apply",), ("builder", "layout", "apply"), has_contract=True, cli_show_effective_context=True, cli_context_write=True),
163
166
  PublicToolSpec(BUILDER_DOMAIN, "app_flow_get_schema", ("app_flow_get_schema",), ("builder", "flow", "schema"), has_contract=True, cli_show_effective_context=True),
164
167
  PublicToolSpec(BUILDER_DOMAIN, "app_flow_get", ("app_flow_get",), ("builder", "flow", "get"), has_contract=True, cli_show_effective_context=True),
165
168
  PublicToolSpec(BUILDER_DOMAIN, "app_flow_apply", ("app_flow_apply",), ("builder", "flow", "apply"), has_contract=True, cli_show_effective_context=True, cli_context_write=True),
@@ -55,6 +55,8 @@ SUCCESS_POLICY_BY_TOOL: dict[str, TransformFn] = {}
55
55
  def trim_public_response(tool_name: str | None, payload: dict[str, Any]) -> dict[str, Any]:
56
56
  if not isinstance(payload, dict):
57
57
  return payload
58
+ if tool_name == "builder:app_form_apply" and str(payload.get("status") or "").lower() == "partial_success":
59
+ return trim_success_response(tool_name, payload)
58
60
  if _looks_like_failure_payload(payload):
59
61
  status = str(payload.get("status") or "").lower()
60
62
  if _is_executed_nonfatal_payload(payload) or (
@@ -83,6 +85,8 @@ def trim_success_response(tool_name: str | None, payload: dict[str, Any]) -> dic
83
85
  drop_keys = COMMON_SUCCESS_DROP_TOP - {"output_profile"}
84
86
  if tool_name in {"user:record_insert", "user:record_update", "user:record_delete"} and payload.get("ok") is False:
85
87
  drop_keys = drop_keys - {"ok"}
88
+ if tool_name == "builder:app_form_apply" and payload.get("mustReadCurrentStateBeforeRepair") is True:
89
+ drop_keys = drop_keys - {"suggested_next_call"}
86
90
  _drop_top_keys(trimmed, drop_keys)
87
91
  transformer = SUCCESS_POLICY_BY_TOOL.get(tool_name or "")
88
92
  if transformer is not None:
@@ -1086,6 +1090,44 @@ def _trim_builder_list_like(payload: JSONObject) -> None:
1086
1090
  _trim_builder_envelope(payload)
1087
1091
 
1088
1092
 
1093
+ def _trim_app_form_apply(payload: JSONObject) -> None:
1094
+ details = payload.get("details")
1095
+ details_diff = deepcopy(details.get("diff")) if isinstance(details, dict) and isinstance(details.get("diff"), dict) else None
1096
+ if isinstance(details, dict):
1097
+ _drop_deep_keys(details, {"transportError"})
1098
+ _trim_builder_envelope(payload)
1099
+ _trim_form_diff(payload)
1100
+ if details_diff is not None:
1101
+ payload.setdefault("details", {})["diff"] = _summarize_form_diff(details_diff)
1102
+
1103
+
1104
+ def _trim_form_diff(payload: JSONObject) -> None:
1105
+ diff = payload.get("diff")
1106
+ if isinstance(diff, dict):
1107
+ payload["diff"] = _summarize_form_diff(diff)
1108
+
1109
+
1110
+ def _summarize_form_diff(diff: dict[str, Any]) -> dict[str, Any]:
1111
+ summary: dict[str, Any] = {}
1112
+ for key in ("added", "updated", "removed"):
1113
+ items = diff.get(key)
1114
+ if not isinstance(items, list):
1115
+ continue
1116
+ names = []
1117
+ for item in items:
1118
+ if isinstance(item, str):
1119
+ names.append(item)
1120
+ elif isinstance(item, dict):
1121
+ name = item.get("name") or item.get("queTitle")
1122
+ if name:
1123
+ names.append(name)
1124
+ summary[key] = names
1125
+ for key in ("layoutChanged", "configChanged"):
1126
+ if key in diff:
1127
+ summary[key] = bool(diff[key])
1128
+ return summary
1129
+
1130
+
1089
1131
  def _register_policy(domains: tuple[str, ...], names: tuple[str, ...], transform: TransformFn) -> None:
1090
1132
  for domain in domains:
1091
1133
  for name in names:
@@ -1190,6 +1232,9 @@ _register_policy(
1190
1232
  "role_create",
1191
1233
  "app_release_edit_lock_if_mine",
1192
1234
  "app_resolve",
1235
+ "app_form_schema",
1236
+ "app_form_get",
1237
+ "app_delete",
1193
1238
  "button_style_catalog_get",
1194
1239
  "app_custom_buttons_apply",
1195
1240
  "app_associated_resources_apply",
@@ -1198,7 +1243,11 @@ _register_policy(
1198
1243
  "app_get_layout",
1199
1244
  "app_get_views",
1200
1245
  "app_get_flow",
1246
+ "app_flow_get_schema",
1247
+ "app_flow_get",
1201
1248
  "app_get_charts",
1249
+ "app_get_buttons",
1250
+ "app_get_associated_resources",
1202
1251
  "app_schema_apply",
1203
1252
  "app_layout_apply",
1204
1253
  "app_flow_apply",
@@ -1210,3 +1259,4 @@ _register_policy(
1210
1259
  ),
1211
1260
  _trim_builder_list_like,
1212
1261
  )
1262
+ _register_policy((BUILDER_DOMAIN,), ("app_form_apply",), _trim_app_form_apply)
@@ -26,12 +26,32 @@ from .tools.workflow_tools import WorkflowTools
26
26
  from .tools.workspace_tools import WorkspaceTools
27
27
 
28
28
 
29
+ def _register_missing_builder_tools(server: FastMCP) -> None:
30
+ """Add current high-level builder tools to the unified MCP without duplicate names."""
31
+ from .server_app_builder import build_builder_server
32
+
33
+ builder_server = build_builder_server()
34
+ registered = server._tool_manager._tools
35
+ for tool in builder_server._tool_manager._tools.values():
36
+ if tool.name in registered:
37
+ continue
38
+ server.add_tool(
39
+ tool.fn,
40
+ name=tool.name,
41
+ title=tool.title,
42
+ description=tool.description,
43
+ annotations=tool.annotations,
44
+ icons=tool.icons,
45
+ meta=tool.meta,
46
+ )
47
+
48
+
29
49
  def build_server() -> FastMCP:
30
50
  today = date.today()
31
51
  current_year = today.year
32
52
  server = FastMCP(
33
53
  "Qingflow MCP",
34
- instructions=f"""Use this server for Qingflow operational workflows. Current date: `{today.isoformat()}`.
54
+ instructions=f"""Use this unified server for Qingflow operational and builder workflows. Current date: `{today.isoformat()}`.
35
55
 
36
56
  ## Authentication
37
57
 
@@ -203,9 +223,9 @@ Normalize relative dates before building DSL.
203
223
 
204
224
  Default to `prod` unless the user explicitly specifies `test`.
205
225
 
206
- ## Constraints
226
+ ## Builder Path
207
227
 
208
- Avoid builder-side app or schema changes here.
228
+ Use the public builder tools for package, AppForm, workflow, view, chart, portal, and application configuration work. Pin the AppForm Schema version, preserve complete form declarations and existing IDs from draft reads, and use `app_form_apply` for reconciliation. Do not fall back to legacy raw builder write routes.
209
229
 
210
230
  ## Feedback Path
211
231
 
@@ -235,6 +255,7 @@ If the current MCP capability is unsupported, the workflow is awkward, or the us
235
255
  WorkflowTools(sessions, backend).register(server)
236
256
  ViewTools(sessions, backend).register(server)
237
257
  SolutionTools(sessions, backend).register(server)
258
+ _register_missing_builder_tools(server)
238
259
  return server
239
260
 
240
261