@qingflow-tech/qingflow-app-builder-mcp 1.0.14 → 1.0.16

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.14
6
+ npm install @qingflow-tech/qingflow-app-builder-mcp@1.0.16
7
7
  ```
8
8
 
9
9
  Run:
10
10
 
11
11
  ```bash
12
- npx -y -p @qingflow-tech/qingflow-app-builder-mcp@1.0.14 qingflow-app-builder-mcp
12
+ npx -y -p @qingflow-tech/qingflow-app-builder-mcp@1.0.16 qingflow-app-builder-mcp
13
13
  ```
14
14
 
15
15
  Environment:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qingflow-tech/qingflow-app-builder-mcp",
3
- "version": "1.0.14",
3
+ "version": "1.0.16",
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.14"
7
+ version = "1.0.16"
8
8
  description = "User-authenticated MCP server for Qingflow"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -11803,6 +11803,96 @@ class AiBuilderFacade:
11803
11803
  "live_result": live_result,
11804
11804
  })
11805
11805
 
11806
+ def portal_delete(self, *, profile: str, dash_key: str) -> JSONObject:
11807
+ normalized_args = {"dash_key": dash_key}
11808
+ dash_key = str(dash_key or "").strip()
11809
+ if not dash_key:
11810
+ return _failed(
11811
+ "PORTAL_DASH_KEY_REQUIRED",
11812
+ "dash_key is required when deleting a portal",
11813
+ normalized_args=normalized_args,
11814
+ missing_fields=["dash_key"],
11815
+ suggested_next_call={"tool_name": "portal_delete", "arguments": {"profile": profile, "dash_key": "DASH_KEY"}},
11816
+ )
11817
+ try:
11818
+ self.portals.portal_delete(profile=profile, dash_key=dash_key)
11819
+ except (QingflowApiError, RuntimeError) as error:
11820
+ api_error = _coerce_api_error(error)
11821
+ return _failed_from_api_error(
11822
+ "PORTAL_DELETE_FAILED",
11823
+ api_error,
11824
+ normalized_args=normalized_args,
11825
+ details={"dash_key": dash_key},
11826
+ suggested_next_call={"tool_name": "portal_delete", "arguments": {"profile": profile, **normalized_args}},
11827
+ )
11828
+
11829
+ delete_readback = self._verify_portal_deleted_by_key(profile=profile, dash_key=dash_key)
11830
+ verified = delete_readback.get("readback_status") == "deleted"
11831
+ return {
11832
+ "status": "success" if verified else "partial_success",
11833
+ "error_code": None if verified else delete_readback.get("error_code") or "PORTAL_DELETE_READBACK_PENDING",
11834
+ "recoverable": not verified,
11835
+ "message": "deleted portal" if verified else "portal delete completed; readback pending",
11836
+ "normalized_args": normalized_args,
11837
+ "missing_fields": [],
11838
+ "allowed_values": {},
11839
+ "details": {} if verified else {"delete_readback": delete_readback},
11840
+ "request_id": delete_readback.get("request_id"),
11841
+ "backend_code": delete_readback.get("backend_code"),
11842
+ "http_status": delete_readback.get("http_status"),
11843
+ "suggested_next_call": None if verified else {"tool_name": "portal_get", "arguments": {"profile": profile, "dash_key": dash_key}},
11844
+ "noop": False,
11845
+ "warnings": [] if verified else [_warning("PORTAL_DELETE_READBACK_PENDING", "portal delete was sent, but deletion readback is not fully verified")],
11846
+ "verification": {"portal_deleted": verified, "delete_readback": delete_readback},
11847
+ "verified": verified,
11848
+ "dash_key": dash_key,
11849
+ "deleted": verified,
11850
+ "delete_executed": True,
11851
+ "readback_status": delete_readback.get("readback_status"),
11852
+ "safe_to_retry_delete": False,
11853
+ "write_executed": True,
11854
+ "safe_to_retry": False,
11855
+ }
11856
+
11857
+ def _verify_portal_deleted_by_key(self, *, profile: str, dash_key: str) -> JSONObject:
11858
+ try:
11859
+ self.portals.portal_get(profile=profile, dash_key=dash_key, being_draft=True)
11860
+ except (QingflowApiError, RuntimeError) as error:
11861
+ api_error = _coerce_api_error(error)
11862
+ if _delete_readback_is_not_found(api_error):
11863
+ return {
11864
+ "dash_key": dash_key,
11865
+ "operation": "delete",
11866
+ "status": "removed",
11867
+ "delete_executed": True,
11868
+ "readback_status": "deleted",
11869
+ "safe_to_retry_delete": False,
11870
+ }
11871
+ return {
11872
+ "dash_key": dash_key,
11873
+ "operation": "delete",
11874
+ "status": "readback_pending",
11875
+ "delete_executed": True,
11876
+ "readback_status": "unavailable",
11877
+ "safe_to_retry_delete": False,
11878
+ "error_code": "PORTAL_DELETE_READBACK_UNAVAILABLE",
11879
+ "message": "delete request completed, but portal existence could not be verified by dash_key readback",
11880
+ "request_id": api_error.request_id,
11881
+ "backend_code": api_error.backend_code,
11882
+ "http_status": None if api_error.http_status == 404 else api_error.http_status,
11883
+ "transport_error": _transport_error_payload(api_error),
11884
+ }
11885
+ return {
11886
+ "dash_key": dash_key,
11887
+ "operation": "delete",
11888
+ "status": "readback_pending",
11889
+ "delete_executed": True,
11890
+ "readback_status": "still_exists",
11891
+ "safe_to_retry_delete": False,
11892
+ "error_code": "PORTAL_DELETE_READBACK_STILL_EXISTS",
11893
+ "message": "delete request completed, but the portal still exists during dash_key readback",
11894
+ }
11895
+
11806
11896
  def _publish_current_edit_version(self, *, profile: str, app_key: str, edit_version_no: int | None = None) -> JSONObject:
11807
11897
  normalized_args = {"app_key": app_key}
11808
11898
  if edit_version_no is None:
@@ -179,6 +179,10 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
179
179
  portal_apply.add_argument("--config-file")
180
180
  portal_apply.set_defaults(handler=_handle_portal_apply, format_hint="builder_summary", force_json_output=True)
181
181
 
182
+ portal_delete = portal_subparsers.add_parser("delete", help="删除门户")
183
+ portal_delete.add_argument("--dash-key", required=True)
184
+ portal_delete.set_defaults(handler=_handle_portal_delete, format_hint="builder_summary", force_json_output=True)
185
+
182
186
  schema_apply = builder_subparsers.add_parser("schema", help="字段搭建")
183
187
  schema_apply_subparsers = schema_apply.add_subparsers(dest="builder_schema_command", required=True)
184
188
  schema_apply_apply = schema_apply_subparsers.add_parser("apply", help="执行字段变更")
@@ -614,6 +618,13 @@ def _handle_portal_apply(args: argparse.Namespace, context: CliContext) -> dict:
614
618
  )
615
619
 
616
620
 
621
+ def _handle_portal_delete(args: argparse.Namespace, context: CliContext) -> dict:
622
+ return context.builder.portal_delete(
623
+ profile=args.profile,
624
+ dash_key=args.dash_key,
625
+ )
626
+
627
+
617
628
  def _handle_publish_verify(args: argparse.Namespace, context: CliContext) -> dict:
618
629
  return context.builder.app_publish_verify(
619
630
  profile=args.profile,
@@ -208,6 +208,8 @@ def _builder_apply_operation_from_args(args: argparse.Namespace) -> str | None:
208
208
  return "app_associated_resources_apply"
209
209
  if section == "portal" and getattr(args, "builder_portal_command", "") == "apply":
210
210
  return "portal_apply"
211
+ if section == "portal" and getattr(args, "builder_portal_command", "") == "delete":
212
+ return "portal_delete"
211
213
  if section == "schema" and getattr(args, "builder_schema_command", "") == "apply":
212
214
  return "app_schema_apply"
213
215
  if section == "layout" and getattr(args, "builder_layout_command", "") == "apply":
@@ -229,7 +231,7 @@ def _builder_apply_operation_from_argv(argv: list[str]) -> str | None:
229
231
  return None
230
232
  section = tokens[1]
231
233
  action = tokens[2]
232
- if action != "apply" and not (section == "publish" and action == "verify"):
234
+ if action != "apply" and not (section == "publish" and action == "verify") and not (section == "portal" and action == "delete"):
233
235
  return None
234
236
  mapping = {
235
237
  "package": "package_apply",
@@ -244,6 +246,8 @@ def _builder_apply_operation_from_argv(argv: list[str]) -> str | None:
244
246
  "charts": "app_charts_apply",
245
247
  "publish": "app_publish_verify",
246
248
  }
249
+ if section == "portal" and action == "delete":
250
+ return "portal_delete"
247
251
  return mapping.get(section)
248
252
 
249
253
 
@@ -162,6 +162,7 @@ BUILDER_PUBLIC_TOOL_SPECS: tuple[PublicToolSpec, ...] = (
162
162
  PublicToolSpec(BUILDER_DOMAIN, "app_views_apply", ("app_views_apply",), ("builder", "views", "apply"), has_contract=True, cli_show_effective_context=True, cli_context_write=True),
163
163
  PublicToolSpec(BUILDER_DOMAIN, "app_charts_apply", ("app_charts_apply",), ("builder", "charts", "apply"), has_contract=True, cli_show_effective_context=True, cli_context_write=True),
164
164
  PublicToolSpec(BUILDER_DOMAIN, "portal_apply", ("portal_apply",), ("builder", "portal", "apply"), has_contract=True, cli_show_effective_context=True, cli_context_write=True),
165
+ PublicToolSpec(BUILDER_DOMAIN, "portal_delete", ("portal_delete",), ("builder", "portal", "delete"), has_contract=True, cli_show_effective_context=True, cli_context_write=True),
165
166
  PublicToolSpec(BUILDER_DOMAIN, "app_publish_verify", ("app_publish_verify",), ("builder", "publish", "verify"), has_contract=True, cli_show_effective_context=True, cli_context_write=True),
166
167
  )
167
168
 
@@ -1203,6 +1203,7 @@ _register_policy(
1203
1203
  "app_views_apply",
1204
1204
  "app_charts_apply",
1205
1205
  "portal_apply",
1206
+ "portal_delete",
1206
1207
  "app_publish_verify",
1207
1208
  ),
1208
1209
  _trim_builder_list_like,
@@ -39,7 +39,7 @@ def build_builder_server() -> FastMCP:
39
39
  "Use workspace_icon_catalog_get before creating app packages, apps, or portals when supported icon/color candidates are needed; new workspace resources require explicit non-template icon + color, and the CLI validates choices without inferring business defaults. "
40
40
  "app_get as the default app map read, then app_get_fields/app_repair_code_blocks/app_get_layout/app_get_views/app_get_flow/app_get_charts/portal_list/portal_get/view_get/chart_get for focused configuration reads, "
41
41
  "member_search/role_search/role_create when workflow assignees must come from the directory or role catalog, preferring roles over explicit members unless the user explicitly names members, "
42
- "then app_schema_apply/app_layout_apply/app_flow_apply/app_views_apply/app_custom_buttons_apply/app_associated_resources_apply/app_charts_apply/portal_apply to execute normalized patches; these apply tools perform planning, normalization, and dependency checks internally where applicable. Schema/layout/views noop requests skip publish, app_custom_buttons_apply and app_associated_resources_apply publish after at least one write succeeds and expose no draft-only parameter, charts are immediate-live without publish and resolve targets by chart_id first then exact unique chart name, portal updates use replace semantics only when sections are supplied and edit-mode base-info-only updates may omit sections, portal pc layout is a 24-column grid and mobile is a 6-column grid so omit position or use layout_preset when unsure, publish=false only guarantees draft/base-info updates for tools that still expose that parameter, and flow should use publish=false whenever you only want draft/precheck behavior. "
42
+ "then app_schema_apply/app_layout_apply/app_flow_apply/app_views_apply/app_custom_buttons_apply/app_associated_resources_apply/app_charts_apply/portal_apply/portal_delete to execute normalized patches; these apply/delete tools perform planning, normalization, and dependency checks internally where applicable. Schema/layout/views noop requests skip publish, app_custom_buttons_apply and app_associated_resources_apply publish after at least one write succeeds and expose no draft-only parameter, charts are immediate-live without publish and resolve targets by chart_id first then exact unique chart name, portal updates use replace semantics only when sections are supplied and edit-mode base-info-only updates may omit sections, portal delete separates DELETE execution from readback verification, portal pc layout is a 24-column grid and mobile is a 6-column grid so omit position or use layout_preset when unsure, publish=false only guarantees draft/base-info updates for tools that still expose that parameter, and flow should use publish=false whenever you only want draft/precheck behavior. "
43
43
  "Builder apply/write outputs include schema_version, operation, summary, and resources[]; use resources[].id/key/name/ids/parent as the stable UI and agent display entry, and keep legacy fields such as field_diff/views_diff/chart_results only for compatibility or troubleshooting. "
44
44
  "For existing object parameter replacement, prefer patch_views, patch_buttons, patch_resources, and patch_charts with set/unset; the tool reads current config and full-saves internally, while upsert_* is for creation or full target configuration and should not be used as an incomplete partial update. "
45
45
  "For builder delete/remove apply results, separate delete execution from readback verification: after DELETE is sent, resources expose delete_executed, readback_status, and safe_to_retry_delete=false. If readback_status is unavailable or still_exists, do not blindly repeat the delete; confirm later with app_get/view_get/chart_get or the relevant apply readback. Views/buttons use single-item readback; associated resources use one app-level resource-pool readback because there is no confirmed single-item GET. "
@@ -595,6 +595,16 @@ def build_builder_server() -> FastMCP:
595
595
  payload=payload,
596
596
  )
597
597
 
598
+ @server.tool()
599
+ def portal_delete(
600
+ profile: str = DEFAULT_PROFILE,
601
+ dash_key: str = "",
602
+ ) -> dict:
603
+ return ai_builder.portal_delete(
604
+ profile=profile,
605
+ dash_key=dash_key,
606
+ )
607
+
598
608
  @server.tool()
599
609
  def app_publish_verify(
600
610
  profile: str = DEFAULT_PROFILE,
@@ -91,6 +91,7 @@ BUILDER_APPLY_TOOL_NAMES = {
91
91
  "app_associated_resources_apply",
92
92
  "app_charts_apply",
93
93
  "portal_apply",
94
+ "portal_delete",
94
95
  "app_publish_verify",
95
96
  }
96
97
 
@@ -553,6 +554,13 @@ class AiBuilderTools(ToolBase):
553
554
  payload=payload,
554
555
  )
555
556
 
557
+ @mcp.tool(description=self._high_risk_tool_description(operation="delete", target="portal"))
558
+ def portal_delete(
559
+ profile: str = DEFAULT_PROFILE,
560
+ dash_key: str = "",
561
+ ) -> JSONObject:
562
+ return self.portal_delete(profile=profile, dash_key=dash_key)
563
+
556
564
  @mcp.tool()
557
565
  def app_publish_verify(
558
566
  profile: str = DEFAULT_PROFILE,
@@ -2600,6 +2608,18 @@ class AiBuilderTools(ToolBase):
2600
2608
  ))
2601
2609
  return _attach_builder_apply_envelope("portal_apply", result)
2602
2610
 
2611
+ @tool_cn_name("门户删除")
2612
+ def portal_delete(self, *, profile: str, dash_key: str) -> JSONObject:
2613
+ """执行门户删除逻辑。"""
2614
+ normalized_args = {"dash_key": dash_key}
2615
+ result = _publicize_package_fields(_safe_tool_call(
2616
+ lambda: self._facade.portal_delete(profile=profile, dash_key=dash_key),
2617
+ error_code="PORTAL_DELETE_FAILED",
2618
+ normalized_args=normalized_args,
2619
+ suggested_next_call={"tool_name": "portal_delete", "arguments": {"profile": profile, **normalized_args}},
2620
+ ))
2621
+ return _attach_builder_apply_envelope("portal_delete", result)
2622
+
2603
2623
  @tool_cn_name("应用发布校验")
2604
2624
  def app_publish_verify(
2605
2625
  self,
@@ -3182,6 +3202,8 @@ def _builder_apply_resources(tool_name: str, payload: JSONObject) -> list[JSONOb
3182
3202
  resources = _builder_chart_resources(payload)
3183
3203
  elif tool_name == "portal_apply":
3184
3204
  resources = _builder_portal_resources(payload)
3205
+ elif tool_name == "portal_delete":
3206
+ resources = _builder_portal_resources(payload, operation_override="removed")
3185
3207
  elif tool_name == "app_custom_buttons_apply":
3186
3208
  resources = _builder_button_resources(payload)
3187
3209
  elif tool_name == "app_associated_resources_apply":
@@ -3377,7 +3399,15 @@ def _builder_package_resources(payload: JSONObject) -> list[JSONObject]:
3377
3399
  package_id = payload.get("package_id") or payload.get("id")
3378
3400
  package_name = payload.get("package_name") or payload.get("name")
3379
3401
  status = _builder_status(payload, "success")
3380
- operation = "failed" if status == "failed" else ("created" if bool(payload.get("created")) else "updated")
3402
+ operation = (
3403
+ "failed"
3404
+ if status == "failed"
3405
+ else "removed"
3406
+ if bool(payload.get("deleted")) or bool(payload.get("delete_executed"))
3407
+ else "created"
3408
+ if bool(payload.get("created"))
3409
+ else "updated"
3410
+ )
3381
3411
  normalized_args = payload.get("normalized_args") if isinstance(payload.get("normalized_args"), dict) else {}
3382
3412
  icon_config = (
3383
3413
  _builder_container_icon_config(payload, raw_keys=("icon", "tagIcon", "tag_icon"))
@@ -3629,7 +3659,7 @@ def _builder_chart_resources(payload: JSONObject) -> list[JSONObject]:
3629
3659
  return resources
3630
3660
 
3631
3661
 
3632
- def _builder_portal_resources(payload: JSONObject) -> list[JSONObject]:
3662
+ def _builder_portal_resources(payload: JSONObject, *, operation_override: str | None = None) -> list[JSONObject]:
3633
3663
  status = _builder_status(payload, "success")
3634
3664
  draft_result = payload.get("draft_result") if isinstance(payload.get("draft_result"), dict) else {}
3635
3665
  live_result = payload.get("live_result") if isinstance(payload.get("live_result"), dict) else {}
@@ -3660,7 +3690,7 @@ def _builder_portal_resources(payload: JSONObject) -> list[JSONObject]:
3660
3690
  first_tag = draft_result.get("tags")[0]
3661
3691
  if isinstance(first_tag, dict):
3662
3692
  package_id = first_tag.get("tagId") or first_tag.get("tag_id") or first_tag.get("id")
3663
- operation = "failed" if status == "failed" else ("created" if bool(payload.get("created")) else "updated")
3693
+ operation = operation_override or ("failed" if status == "failed" else ("created" if bool(payload.get("created")) else "updated"))
3664
3694
  parent = None
3665
3695
  if package_id:
3666
3696
  parent = _builder_parent("package", id_value=package_id, key=package_id)
@@ -5507,6 +5537,20 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
5507
5537
  },
5508
5538
  },
5509
5539
  },
5540
+ "portal_delete": {
5541
+ "allowed_keys": ["dash_key"],
5542
+ "aliases": {"dashKey": "dash_key"},
5543
+ "allowed_values": {},
5544
+ "execution_notes": [
5545
+ "deletes one portal by dash_key",
5546
+ "delete results separate DELETE execution from readback verification",
5547
+ "if delete_executed=true and readback_status=unavailable or still_exists, do not blindly repeat delete; confirm later with portal_get or portal_list",
5548
+ ],
5549
+ "minimal_example": {
5550
+ "profile": "default",
5551
+ "dash_key": "DASH_KEY",
5552
+ },
5553
+ },
5510
5554
  "app_publish_verify": {
5511
5555
  "allowed_keys": ["app_key", "expected_package_id"],
5512
5556
  "aliases": {},