@qingflow-tech/qingflow-app-builder-mcp 1.0.41 → 1.0.43

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -3,13 +3,13 @@
3
3
  Install:
4
4
 
5
5
  ```bash
6
- npm install @qingflow-tech/qingflow-app-builder-mcp@1.0.41
6
+ npm install @qingflow-tech/qingflow-app-builder-mcp@1.0.43
7
7
  ```
8
8
 
9
9
  Run:
10
10
 
11
11
  ```bash
12
- npx -y -p @qingflow-tech/qingflow-app-builder-mcp@1.0.41 qingflow-app-builder-mcp
12
+ npx -y -p @qingflow-tech/qingflow-app-builder-mcp@1.0.43 qingflow-app-builder-mcp
13
13
  ```
14
14
 
15
15
  Environment:
@@ -128,7 +128,7 @@ qingflow-skills install --agent codex --scope user
128
128
  如果通过一次性 `npx -p <package>` 执行安装,请加 `--copy`,避免 symlink 指向 npm 临时执行缓存:
129
129
 
130
130
  ```bash
131
- npx -y -p @josephyan/qingflow-cli qingflow-skills install --agent codex --scope user --copy
131
+ npx -y -p @qingflow-tech/qingflow-cli qingflow-skills install --agent codex --scope user --copy
132
132
  ```
133
133
 
134
134
  也可以挂载到项目级 agent 目录:
@@ -268,7 +268,7 @@ qingflow-app-builder-mcp-skills list
268
268
  "command": "npx",
269
269
  "args": [
270
270
  "-y",
271
- "@josephyan/qingflow-app-user-mcp"
271
+ "@qingflow-tech/qingflow-app-user-mcp"
272
272
  ],
273
273
  "env": {
274
274
  "QINGFLOW_MCP_DEFAULT_BASE_URL": "https://qingflow.com/api",
@@ -281,7 +281,7 @@ qingflow-app-builder-mcp-skills list
281
281
  "command": "npx",
282
282
  "args": [
283
283
  "-y",
284
- "@josephyan/qingflow-app-builder-mcp"
284
+ "@qingflow-tech/qingflow-app-builder-mcp"
285
285
  ],
286
286
  "env": {
287
287
  "QINGFLOW_MCP_DEFAULT_BASE_URL": "https://qingflow.com/api",
@@ -323,7 +323,7 @@ npm install
323
323
 
324
324
  如果 MCP 客户端一调用工具就报 `Transport closed`,优先检查这几件事:
325
325
 
326
- 1. 不要混用不同版本的 `@josephyan/qingflow-cli`、`@josephyan/qingflow-app-user-mcp`、`@josephyan/qingflow-app-builder-mcp`
326
+ 1. 不要混用不同版本的 `@qingflow-tech/qingflow-cli`、`@qingflow-tech/qingflow-app-user-mcp`、`@qingflow-tech/qingflow-app-builder-mcp`
327
327
  2. 删除安装目录下的 `.npm-python`
328
328
  3. 重新执行 `npm install` 或重新安装对应 tgz/npm 包
329
329
  4. 再启动 MCP 客户端
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qingflow-tech/qingflow-app-builder-mcp",
3
- "version": "1.0.41",
3
+ "version": "1.0.43",
4
4
  "description": "Builder MCP for Qingflow app/package/system design and staged solution workflows.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/pyproject.toml CHANGED
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "qingflow-mcp"
7
- version = "1.0.41"
7
+ version = "1.0.43"
8
8
  description = "User-authenticated MCP server for Qingflow"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -7,7 +7,7 @@ metadata:
7
7
 
8
8
  # Qingflow App Builder
9
9
 
10
- > **Skill 版本**:`qingflow-skills-2026.06.23.1`(入口文档版本;如需确认 CLI 包版本,使用 `qingflow --version` 或 `qingflow --json version`)。
10
+ > **Skill 版本**:`qingflow-skills-2026.06.24.1`(入口文档版本;如需确认 CLI 包版本,使用 `qingflow --version` 或 `qingflow --json version`)。
11
11
 
12
12
  ## Overview
13
13
 
@@ -7,7 +7,7 @@ metadata:
7
7
 
8
8
  # Qingflow App Builder Code Integrations
9
9
 
10
- > **Skill 版本**:`qingflow-skills-2026.06.23.1`(入口文档版本;如需确认 CLI 包版本,使用 `qingflow --version` 或 `qingflow --json version`)。
10
+ > **Skill 版本**:`qingflow-skills-2026.06.24.1`(入口文档版本;如需确认 CLI 包版本,使用 `qingflow --version` 或 `qingflow --json version`)。
11
11
 
12
12
  Use this skill when the user wants to build or repair:
13
13
  - `code_block` fields
@@ -650,6 +650,42 @@ class AiBuilderFacade:
650
650
  normalized_args=normalized_args,
651
651
  )
652
652
  if layout_result.get("status") not in {"success", "partial_success"}:
653
+ prior_package_write_executed = bool(
654
+ created
655
+ or (
656
+ metadata_requested
657
+ and isinstance(update_result, dict)
658
+ and update_result.get("status") in {"success", "partial_success"}
659
+ and not bool(update_result.get("noop"))
660
+ )
661
+ )
662
+ if prior_package_write_executed:
663
+ layout_details = layout_result.get("details") if isinstance(layout_result.get("details"), dict) else {}
664
+ partial = _post_write_readback_pending_result(
665
+ error_code=str(layout_result.get("error_code") or "PACKAGE_APPLY_PARTIAL"),
666
+ message="created or updated package, but package layout apply failed; read package before retrying",
667
+ normalized_args=normalized_args,
668
+ details={
669
+ "package_id": effective_package_id,
670
+ "layout_error_code": layout_result.get("error_code"),
671
+ "layout_result": layout_result,
672
+ **(
673
+ {"layout_write_error": layout_details.get("write_error")}
674
+ if isinstance(layout_details.get("write_error"), dict)
675
+ else {}
676
+ ),
677
+ },
678
+ suggested_next_call={
679
+ "tool_name": "package_get",
680
+ "arguments": {"profile": profile, "package_id": effective_package_id},
681
+ },
682
+ request_id=layout_result.get("request_id") if isinstance(layout_result.get("request_id"), str) else None,
683
+ backend_code=layout_result.get("backend_code"),
684
+ http_status=layout_result.get("http_status") if isinstance(layout_result.get("http_status"), int) else None,
685
+ )
686
+ partial["package_id"] = effective_package_id
687
+ partial["layout_failed"] = True
688
+ return _apply_permission_outcomes(partial, *permission_outcomes)
653
689
  return _apply_permission_outcomes(layout_result, *permission_outcomes)
654
690
 
655
691
  write_executed = bool(
@@ -13881,7 +13917,7 @@ def _failed_from_api_error(
13881
13917
  "category": error.category,
13882
13918
  },
13883
13919
  )
13884
- return _failed(
13920
+ result = _failed(
13885
13921
  effective_error_code,
13886
13922
  public_message,
13887
13923
  recoverable=recoverable,
@@ -13894,6 +13930,14 @@ def _failed_from_api_error(
13894
13930
  backend_code=error.backend_code,
13895
13931
  http_status=public_http_status,
13896
13932
  )
13933
+ if _is_environment_quota_code(error.backend_code):
13934
+ _mark_environment_quota_block(
13935
+ result,
13936
+ write_executed=False,
13937
+ next_action="retry_after_quota_restored",
13938
+ message="backend quota/AI assistant limit blocked this operation; retry after quota is restored",
13939
+ )
13940
+ return result
13897
13941
 
13898
13942
 
13899
13943
  def _post_write_readback_pending_result(
@@ -13947,6 +13991,15 @@ def _post_write_readback_pending_result(
13947
13991
  "next_action": "readback_before_retry",
13948
13992
  }
13949
13993
  result["verification"]["readback_before_retry"] = True
13994
+ if _is_environment_quota_code(effective_backend_code):
13995
+ result["readback_blocked_by_environment"] = True
13996
+ result["verification"]["readback_blocked_by_environment"] = True
13997
+ _mark_environment_quota_block(
13998
+ result,
13999
+ write_executed=True,
14000
+ next_action="retry_after_quota_restored",
14001
+ message="post-write readback was blocked by backend quota/AI assistant limit; retry readback after quota is restored",
14002
+ )
13950
14003
  return result
13951
14004
 
13952
14005
 
@@ -13974,7 +14027,7 @@ def _post_write_may_have_succeeded_result(
13974
14027
  ):
13975
14028
  if value is not None:
13976
14029
  warning[key] = value
13977
- return {
14030
+ result = {
13978
14031
  "status": "partial_success",
13979
14032
  "error_code": error_code,
13980
14033
  "recoverable": True,
@@ -14001,6 +14054,16 @@ def _post_write_may_have_succeeded_result(
14001
14054
  "safe_to_retry": False,
14002
14055
  "next_action": "readback_before_retry",
14003
14056
  }
14057
+ if _is_environment_quota_code(effective_backend_code):
14058
+ result["readback_blocked_by_environment"] = True
14059
+ result["verification"]["readback_blocked_by_environment"] = True
14060
+ _mark_environment_quota_block(
14061
+ result,
14062
+ write_executed=True,
14063
+ next_action="retry_after_quota_restored",
14064
+ message="write result or readback was blocked by backend quota/AI assistant limit; retry after quota is restored",
14065
+ )
14066
+ return result
14004
14067
 
14005
14068
 
14006
14069
  def _readback_transport_error_from_details(details: JSONObject) -> JSONObject | None:
@@ -14047,6 +14110,43 @@ def _transport_error_payload(error: QingflowApiError) -> JSONObject:
14047
14110
  }
14048
14111
 
14049
14112
 
14113
+ def _is_environment_quota_code(code: Any) -> bool:
14114
+ return backend_code_value_int(code) == 59004
14115
+
14116
+
14117
+ def _mark_environment_quota_block(
14118
+ payload: JSONObject,
14119
+ *,
14120
+ write_executed: bool,
14121
+ next_action: str,
14122
+ message: str,
14123
+ ) -> None:
14124
+ payload["environment_blocked"] = True
14125
+ payload["blocker_type"] = "quota_limit"
14126
+ payload["next_action"] = next_action
14127
+ payload["safe_to_retry"] = False
14128
+ payload["write_executed"] = bool(write_executed)
14129
+ details = payload.get("details")
14130
+ if not isinstance(details, dict):
14131
+ details = {}
14132
+ payload["details"] = details
14133
+ details.setdefault("environment_blocked", True)
14134
+ details.setdefault("blocker_type", "quota_limit")
14135
+ details.setdefault("next_action", next_action)
14136
+ details.setdefault("fix_hint", message)
14137
+ warnings = payload.get("warnings")
14138
+ if not isinstance(warnings, list):
14139
+ warnings = []
14140
+ payload["warnings"] = warnings
14141
+ if not any(isinstance(item, dict) and item.get("code") == "ENVIRONMENT_QUOTA_LIMIT" for item in warnings):
14142
+ warning = _warning("ENVIRONMENT_QUOTA_LIMIT", message)
14143
+ for key in ("backend_code", "http_status", "request_id"):
14144
+ value = payload.get(key)
14145
+ if value is not None:
14146
+ warning[key] = value
14147
+ warnings.append(warning)
14148
+
14149
+
14050
14150
  def _is_uncertain_write_transport_error(error: QingflowApiError) -> bool:
14051
14151
  if is_auth_like_error(error):
14052
14152
  return False
@@ -5,7 +5,7 @@ from copy import deepcopy
5
5
 
6
6
  from ..context import CliContext
7
7
  from ..json_io import load_json_value
8
- from .common import load_list_arg, load_object_arg, raise_config_error, require_list_arg
8
+ from .common import load_list_arg, load_object_arg, parse_bool_text, raise_config_error, require_list_arg
9
9
 
10
10
 
11
11
  def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
@@ -196,7 +196,7 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
196
196
  schema_apply_apply.add_argument("--color")
197
197
  schema_apply_apply.add_argument("--visibility-file")
198
198
  schema_apply_apply.add_argument("--create-if-missing", action="store_true")
199
- schema_apply_apply.add_argument("--publish", action=argparse.BooleanOptionalAction, default=True)
199
+ schema_apply_apply.add_argument("--publish", action=argparse.BooleanOptionalAction, default=None)
200
200
  schema_apply_apply.add_argument("--apps-file", help="多应用 schema JSON 数组;每项可带 client_key/app_name/add_fields,支持 relation target_app_ref")
201
201
  schema_apply_apply.add_argument("--add-fields-file", help="字段 JSON 数组;字段可用 as_data_title/as_data_cover 标记数据标题/封面")
202
202
  schema_apply_apply.add_argument("--update-fields-file", help="字段更新 JSON 数组;set 内可用 as_data_title/as_data_cover 标记数据标题/封面")
@@ -483,8 +483,20 @@ def _handle_schema_apply(args: argparse.Namespace, context: CliContext) -> dict:
483
483
  apps_warnings = apps_payload.get("warnings") or []
484
484
  package_id = args.package_id
485
485
  if package_id is None and apps_payload.get("package_id") is not None:
486
- package_id = int(apps_payload["package_id"])
486
+ package_id = _coerce_apps_file_package_id(apps_payload["package_id"])
487
487
  if args.apps_file:
488
+ file_create_if_missing = _coerce_apps_file_bool(
489
+ apps_payload.get("create_if_missing"),
490
+ field_name="create_if_missing",
491
+ default=False,
492
+ )
493
+ file_publish = _coerce_apps_file_bool(
494
+ apps_payload.get("publish"),
495
+ field_name="publish",
496
+ default=True,
497
+ )
498
+ effective_create_if_missing = bool(args.create_if_missing or file_create_if_missing)
499
+ effective_publish = bool(args.publish if args.publish is not None else file_publish)
488
500
  if not apps:
489
501
  raise_config_error(
490
502
  "schema apply multi-app mode requires a non-empty --apps-file.",
@@ -505,8 +517,8 @@ def _handle_schema_apply(args: argparse.Namespace, context: CliContext) -> dict:
505
517
  profile=args.profile,
506
518
  package_id=package_id,
507
519
  visibility=load_object_arg(args.visibility_file, option_name="--visibility-file"),
508
- create_if_missing=bool(args.create_if_missing),
509
- publish=bool(args.publish),
520
+ create_if_missing=effective_create_if_missing,
521
+ publish=effective_publish,
510
522
  apps=apps,
511
523
  add_fields=[],
512
524
  update_fields=[],
@@ -543,7 +555,7 @@ def _handle_schema_apply(args: argparse.Namespace, context: CliContext) -> dict:
543
555
  color=args.color,
544
556
  visibility=load_object_arg(args.visibility_file, option_name="--visibility-file"),
545
557
  create_if_missing=bool(args.create_if_missing),
546
- publish=bool(args.publish),
558
+ publish=True if args.publish is None else bool(args.publish),
547
559
  add_fields=load_list_arg(args.add_fields_file, option_name="--add-fields-file"),
548
560
  update_fields=load_list_arg(args.update_fields_file, option_name="--update-fields-file"),
549
561
  remove_fields=load_list_arg(args.remove_fields_file, option_name="--remove-fields-file"),
@@ -595,6 +607,10 @@ def _load_apps_file_arg(path: str | None) -> dict[str, object]:
595
607
  }
596
608
  if wrapper.get("package_id") is not None:
597
609
  result["package_id"] = wrapper.get("package_id")
610
+ if wrapper.get("create_if_missing") is not None:
611
+ result["create_if_missing"] = wrapper.get("create_if_missing")
612
+ if wrapper.get("publish") is not None:
613
+ result["publish"] = wrapper.get("publish")
598
614
  return result
599
615
  if any(isinstance(item, dict) and "apps" in item for item in payload):
600
616
  raise_config_error(
@@ -616,6 +632,10 @@ def _load_apps_file_arg(path: str | None) -> dict[str, object]:
616
632
  result: dict[str, object] = {"apps": apps}
617
633
  if payload.get("package_id") is not None:
618
634
  result["package_id"] = payload.get("package_id")
635
+ if payload.get("create_if_missing") is not None:
636
+ result["create_if_missing"] = payload.get("create_if_missing")
637
+ if payload.get("publish") is not None:
638
+ result["publish"] = payload.get("publish")
619
639
  return result
620
640
  raise_config_error(
621
641
  "--apps-file must be a JSON array or an object containing apps.",
@@ -625,6 +645,60 @@ def _load_apps_file_arg(path: str | None) -> dict[str, object]:
625
645
  )
626
646
 
627
647
 
648
+ def _coerce_apps_file_package_id(value: object) -> int:
649
+ package_id: int
650
+ if isinstance(value, bool):
651
+ _raise_apps_file_package_id_invalid(value)
652
+ if isinstance(value, int):
653
+ package_id = value
654
+ elif isinstance(value, str):
655
+ stripped = value.strip()
656
+ try:
657
+ package_id = int(stripped)
658
+ except ValueError:
659
+ _raise_apps_file_package_id_invalid(value)
660
+ else:
661
+ _raise_apps_file_package_id_invalid(value)
662
+ if package_id <= 0:
663
+ _raise_apps_file_package_id_invalid(value)
664
+ return package_id
665
+
666
+
667
+ def _raise_apps_file_package_id_invalid(value: object) -> None:
668
+ raise_config_error(
669
+ "--apps-file package_id must be a positive integer.",
670
+ fix_hint='Use a numeric package_id, for example {"package_id":1001,"apps":[...]}.',
671
+ error_code="APPS_FILE_PACKAGE_ID_INVALID",
672
+ details={
673
+ "field": "package_id",
674
+ "value": value,
675
+ "expected": "positive integer or numeric string",
676
+ },
677
+ )
678
+
679
+
680
+ def _coerce_apps_file_bool(value: object, *, field_name: str, default: bool) -> bool:
681
+ if value is None:
682
+ return default
683
+ if isinstance(value, bool):
684
+ return value
685
+ if isinstance(value, str):
686
+ try:
687
+ return parse_bool_text(value)
688
+ except argparse.ArgumentTypeError:
689
+ pass
690
+ raise_config_error(
691
+ f"--apps-file {field_name} must be a boolean.",
692
+ fix_hint=f'Use "{field_name}": true or "{field_name}": false in --apps-file.',
693
+ error_code="APPS_FILE_BOOLEAN_INVALID",
694
+ details={
695
+ "field": field_name,
696
+ "value": value,
697
+ "expected": "boolean true/false or string true/false/1/0/yes/no",
698
+ },
699
+ )
700
+
701
+
628
702
  def _handle_layout_apply(args: argparse.Namespace, context: CliContext) -> dict:
629
703
  return context.builder.app_layout_apply(
630
704
  profile=args.profile,
@@ -100,7 +100,7 @@ def trim_error_response(payload: dict[str, Any]) -> dict[str, Any]:
100
100
  details = trimmed.get("details")
101
101
  if isinstance(details, dict):
102
102
  preserved = {}
103
- for key in ("blocking_issues", "compiled_match_rules"):
103
+ for key in ("blocking_issues", "compiled_match_rules", "issues", "expected_shape"):
104
104
  if key in details:
105
105
  preserved[key] = details.get(key)
106
106
  compact_details = _compact_scalar_dict(details)
@@ -184,7 +184,7 @@ def _trim_returned_failure(payload: dict[str, Any]) -> dict[str, Any]:
184
184
  details = trimmed.get("details")
185
185
  if isinstance(details, dict):
186
186
  preserved = {}
187
- for key in ("blocking_issues", "compiled_match_rules"):
187
+ for key in ("blocking_issues", "compiled_match_rules", "issues", "expected_shape"):
188
188
  if key in details:
189
189
  preserved[key] = details.get(key)
190
190
  compact_details = _compact_scalar_dict(details)
@@ -1071,8 +1071,9 @@ def _trim_builder_envelope(payload: JSONObject) -> None:
1071
1071
  if isinstance(details, dict):
1072
1072
  _drop_deep_keys(details, {"request_route", "base_url", "normalized_args", "suggested_next_call", "transport", "response", "body", "raw"})
1073
1073
  preserved = {}
1074
- if isinstance(details.get("compiled_match_rules"), dict):
1075
- preserved["compiled_match_rules"] = details.get("compiled_match_rules")
1074
+ for key in ("compiled_match_rules", "issues", "expected_shape"):
1075
+ if key in details:
1076
+ preserved[key] = details.get(key)
1076
1077
  compact = _compact_scalar_dict(details)
1077
1078
  compact.update(preserved)
1078
1079
  if compact: