@josephyan/qingflow-app-builder-mcp 0.2.0-beta.987 → 0.2.0-beta.988

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 @josephyan/qingflow-app-builder-mcp@0.2.0-beta.987
6
+ npm install @josephyan/qingflow-app-builder-mcp@0.2.0-beta.988
7
7
  ```
8
8
 
9
9
  Run:
10
10
 
11
11
  ```bash
12
- npx -y -p @josephyan/qingflow-app-builder-mcp@0.2.0-beta.987 qingflow-app-builder-mcp
12
+ npx -y -p @josephyan/qingflow-app-builder-mcp@0.2.0-beta.988 qingflow-app-builder-mcp
13
13
  ```
14
14
 
15
15
  Environment:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@josephyan/qingflow-app-builder-mcp",
3
- "version": "0.2.0-beta.987",
3
+ "version": "0.2.0-beta.988",
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 = "0.2.0b987"
7
+ version = "0.2.0b988"
8
8
  description = "User-authenticated MCP server for Qingflow"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -40,6 +40,7 @@ Default modeling rules:
40
40
  - Directory: `member_search`, `role_search`, `role_create`
41
41
  - Writes: `app_schema_apply`, `app_layout_apply`, `app_flow_apply`, `app_views_apply`, `app_charts_apply`, `portal_apply`, `app_release_edit_lock_if_mine`
42
42
  - Verification: `app_publish_verify`
43
+ - Cross-cutting escalation: `feedback_submit` after explicit user confirmation when the public builder surface still cannot satisfy the user's need
43
44
 
44
45
  Treat these as the official surface. Do not default to `package_create`, `package_attach_app`, raw `portal_*` writes, or raw `qingbi_report_*` writes.
45
46
 
@@ -102,6 +103,7 @@ Treat these as the official surface. Do not default to `package_create`, `packag
102
103
  - For workflow assignees, prefer `role_search` over explicit members unless the user explicitly wants named members.
103
104
  - Public flow building is still intentionally limited to stable linear workflows. If a requirement sounds like branches/conditions, explain the limitation instead of freehanding unsupported graph shapes.
104
105
  - Respect collaborative edit locks. Only use `app_release_edit_lock_if_mine` when the lock owner is the current authenticated user.
106
+ - If the supported builder surface is still awkward or blocked after reasonable use, summarize the gap, ask whether to submit feedback, and call `feedback_submit` only after explicit user confirmation.
105
107
 
106
108
  ## Response Interpretation
107
109
 
@@ -5,7 +5,7 @@ from pathlib import Path
5
5
 
6
6
  __all__ = ["__version__"]
7
7
 
8
- _FALLBACK_VERSION = "0.2.0b987"
8
+ _FALLBACK_VERSION = "0.2.0b988"
9
9
 
10
10
 
11
11
  def _resolve_local_pyproject_version() -> str | None:
@@ -13454,6 +13454,7 @@ def _canonicalize_reference_questions_for_save(
13454
13454
  source: dict[str, Any],
13455
13455
  field: dict[str, Any],
13456
13456
  ) -> list[dict[str, Any]]:
13457
+ relation_config_explicit = bool(field.get("_relation_config_explicit"))
13457
13458
  normalized_source_questions = [
13458
13459
  item
13459
13460
  for item in (
@@ -13462,6 +13463,8 @@ def _canonicalize_reference_questions_for_save(
13462
13463
  )
13463
13464
  if item is not None
13464
13465
  ]
13466
+ if not relation_config_explicit:
13467
+ return normalized_source_questions
13465
13468
 
13466
13469
  display_field = field.get("display_field") if isinstance(field.get("display_field"), dict) else None
13467
13470
  visible_fields = [item for item in cast(list[Any], field.get("visible_fields") or []) if isinstance(item, dict)]
@@ -13498,14 +13501,19 @@ def _canonicalize_reference_questions_for_save(
13498
13501
  if matched_index is not None:
13499
13502
  used_source_indexes.add(matched_index)
13500
13503
 
13501
- next_ordinal = len(canonical_questions) + 1
13502
- for index, item in enumerate(normalized_source_questions):
13503
- if index in used_source_indexes:
13504
- continue
13505
- remaining_item = deepcopy(item)
13506
- remaining_item["ordinal"] = next_ordinal
13507
- next_ordinal += 1
13508
- canonical_questions.append(remaining_item)
13504
+ source_target_app_key = str(source.get("referAppKey") or "").strip()
13505
+ target_app_key = str(field.get("target_app_key") or "").strip()
13506
+ preserve_remaining_source_questions = not source_target_app_key or source_target_app_key == target_app_key
13507
+
13508
+ if preserve_remaining_source_questions:
13509
+ next_ordinal = len(canonical_questions) + 1
13510
+ for index, item in enumerate(normalized_source_questions):
13511
+ if index in used_source_indexes:
13512
+ continue
13513
+ remaining_item = deepcopy(item)
13514
+ remaining_item["ordinal"] = next_ordinal
13515
+ next_ordinal += 1
13516
+ canonical_questions.append(remaining_item)
13509
13517
 
13510
13518
  return canonical_questions
13511
13519
 
@@ -13514,6 +13522,7 @@ def _canonicalize_reference_auth_questions_for_save(
13514
13522
  *,
13515
13523
  source: dict[str, Any],
13516
13524
  refer_questions: list[dict[str, Any]],
13525
+ relation_config_explicit: bool,
13517
13526
  ) -> list[dict[str, Any]]:
13518
13527
  source_auth_questions = [
13519
13528
  item
@@ -13530,6 +13539,40 @@ def _canonicalize_reference_auth_questions_for_save(
13530
13539
  continue
13531
13540
  source_auth_by_que_id[que_id] = item
13532
13541
 
13542
+ if not relation_config_explicit:
13543
+ auth_questions: list[dict[str, Any]] = []
13544
+ seen_que_ids: set[int] = set()
13545
+ refer_question_auth_by_que_id: dict[int, int] = {}
13546
+ for item in refer_questions:
13547
+ que_id = _coerce_any_int(item.get("queId"))
13548
+ que_auth = _coerce_nonnegative_int(item.get("queAuth"))
13549
+ if que_id is None or que_auth is None or que_id in refer_question_auth_by_que_id:
13550
+ continue
13551
+ refer_question_auth_by_que_id[que_id] = que_auth
13552
+
13553
+ for item in source_auth_questions:
13554
+ que_id = _coerce_any_int(item.get("queId"))
13555
+ if que_id is None or que_id in seen_que_ids:
13556
+ continue
13557
+ payload = deepcopy(item)
13558
+ if que_id in refer_question_auth_by_que_id:
13559
+ payload["queAuth"] = refer_question_auth_by_que_id[que_id]
13560
+ auth_questions.append(payload)
13561
+ seen_que_ids.add(que_id)
13562
+
13563
+ for item in refer_questions:
13564
+ que_id = _coerce_any_int(item.get("queId"))
13565
+ que_auth = _coerce_nonnegative_int(item.get("queAuth"))
13566
+ if que_id is None or que_auth is None or que_id in seen_que_ids:
13567
+ continue
13568
+ payload = deepcopy(source_auth_by_que_id.get(que_id) or {"queId": que_id})
13569
+ payload["queId"] = que_id
13570
+ payload["queAuth"] = que_auth
13571
+ auth_questions.append(payload)
13572
+ seen_que_ids.add(que_id)
13573
+
13574
+ return _dedupe_reference_auth_questions(auth_questions)
13575
+
13533
13576
  auth_questions: list[dict[str, Any]] = []
13534
13577
  for item in refer_questions:
13535
13578
  que_id = _coerce_any_int(item.get("queId"))
@@ -13548,6 +13591,7 @@ def _enforce_reference_config_consistency_for_save(
13548
13591
  *,
13549
13592
  field: dict[str, Any],
13550
13593
  ) -> dict[str, Any]:
13594
+ relation_config_explicit = bool(field.get("_relation_config_explicit"))
13551
13595
  refer_questions = [
13552
13596
  item
13553
13597
  for item in (
@@ -13583,28 +13627,29 @@ def _enforce_reference_config_consistency_for_save(
13583
13627
  if display_field_que_id is not None:
13584
13628
  payload["referQueId"] = display_field_que_id
13585
13629
 
13586
- if display_field_que_id is not None and not any(
13587
- _coerce_any_int(item.get("queId")) == display_field_que_id for item in refer_questions
13588
- ):
13589
- display_selector = field.get("display_field") if isinstance(field.get("display_field"), dict) else None
13590
- display_question = (
13591
- _build_reference_question_from_visible_selector(display_selector, ordinal=1)
13592
- if display_selector is not None
13593
- else None
13594
- )
13595
- if display_question is not None:
13596
- display_question["queId"] = display_field_que_id
13597
- display_question["queAuth"] = _REFERENCE_FIELD_VISIBLE_AUTH
13598
- refer_questions = [display_question, *refer_questions]
13630
+ if relation_config_explicit:
13631
+ if display_field_que_id is not None and not any(
13632
+ _coerce_any_int(item.get("queId")) == display_field_que_id for item in refer_questions
13633
+ ):
13634
+ display_selector = field.get("display_field") if isinstance(field.get("display_field"), dict) else None
13635
+ display_question = (
13636
+ _build_reference_question_from_visible_selector(display_selector, ordinal=1)
13637
+ if display_selector is not None
13638
+ else None
13639
+ )
13640
+ if display_question is not None:
13641
+ display_question["queId"] = display_field_que_id
13642
+ display_question["queAuth"] = _REFERENCE_FIELD_VISIBLE_AUTH
13643
+ refer_questions = [display_question, *refer_questions]
13599
13644
 
13600
- if display_field_que_id is not None:
13601
- display_questions = [
13602
- item for item in refer_questions if _coerce_any_int(item.get("queId")) == display_field_que_id
13603
- ]
13604
- trailing_questions = [
13605
- item for item in refer_questions if _coerce_any_int(item.get("queId")) != display_field_que_id
13606
- ]
13607
- refer_questions = [*display_questions, *trailing_questions]
13645
+ if display_field_que_id is not None:
13646
+ display_questions = [
13647
+ item for item in refer_questions if _coerce_any_int(item.get("queId")) == display_field_que_id
13648
+ ]
13649
+ trailing_questions = [
13650
+ item for item in refer_questions if _coerce_any_int(item.get("queId")) != display_field_que_id
13651
+ ]
13652
+ refer_questions = [*display_questions, *trailing_questions]
13608
13653
 
13609
13654
  for ordinal, item in enumerate(refer_questions, start=1):
13610
13655
  que_id = _coerce_any_int(item.get("queId"))
@@ -13622,6 +13667,7 @@ def _enforce_reference_config_consistency_for_save(
13622
13667
  payload["referAuthQues"] = _canonicalize_reference_auth_questions_for_save(
13623
13668
  source={"referAuthQues": refer_auth_ques},
13624
13669
  refer_questions=refer_questions,
13670
+ relation_config_explicit=relation_config_explicit,
13625
13671
  )
13626
13672
  return payload
13627
13673
 
@@ -13661,7 +13707,11 @@ def _normalize_reference_config_for_save(
13661
13707
  if refer_fill_rules or "referFillRules" in source:
13662
13708
  payload["referFillRules"] = refer_fill_rules
13663
13709
 
13664
- refer_auth_ques = _canonicalize_reference_auth_questions_for_save(source=source, refer_questions=refer_questions)
13710
+ refer_auth_ques = _canonicalize_reference_auth_questions_for_save(
13711
+ source=source,
13712
+ refer_questions=refer_questions,
13713
+ relation_config_explicit=bool(field.get("_relation_config_explicit")),
13714
+ )
13665
13715
  if not refer_auth_ques:
13666
13716
  refer_auth_ques = _synthesize_reference_auth_questions_for_save(source=source, field=field)
13667
13717
  if refer_auth_ques or "referAuthQues" in source:
@@ -13917,6 +13967,13 @@ def _field_to_question(field: dict[str, Any], *, temp_id: int) -> dict[str, Any]
13917
13967
  if isinstance(built_question.get("referenceConfig"), dict)
13918
13968
  else {}
13919
13969
  )
13970
+ original_target_app_key = str(existing_reference.get("referAppKey") or "").strip()
13971
+ next_target_app_key = str(field.get("target_app_key") or "").strip()
13972
+ preserve_existing_reference_questions = (
13973
+ relation_config_explicit
13974
+ and bool(original_target_app_key)
13975
+ and original_target_app_key == next_target_app_key
13976
+ )
13920
13977
  if relation_config_explicit:
13921
13978
  for stale_key in ("customButtonText", "customAdvancedSetting", "configShowForm", "dataShowForm"):
13922
13979
  reference.pop(stale_key, None)
@@ -13928,7 +13985,7 @@ def _field_to_question(field: dict[str, Any], *, temp_id: int) -> dict[str, Any]
13928
13985
  "fieldNameShow",
13929
13986
  "_targetFieldId",
13930
13987
  ):
13931
- if relation_config_explicit and key in {"referQuestions", "referAuthQues"}:
13988
+ if preserve_existing_reference_questions and key in {"referQuestions", "referAuthQues"}:
13932
13989
  continue
13933
13990
  if key in built_reference:
13934
13991
  reference[key] = deepcopy(built_reference[key])
@@ -93,6 +93,16 @@ def build_builder_server() -> FastMCP:
93
93
  include_external=include_external,
94
94
  )
95
95
 
96
+ @server.tool()
97
+ def workspace_get(
98
+ profile: str = DEFAULT_PROFILE,
99
+ ws_id: int | None = None,
100
+ ) -> dict:
101
+ return workspace.workspace_get(
102
+ profile=profile,
103
+ ws_id=ws_id,
104
+ )
105
+
96
106
  @server.tool()
97
107
  def file_upload_local(
98
108
  profile: str = DEFAULT_PROFILE,
@@ -228,6 +228,16 @@ If the current MCP capability is unsupported, the workflow is awkward, or the us
228
228
  include_external=include_external,
229
229
  )
230
230
 
231
+ @server.tool()
232
+ def workspace_get(
233
+ profile: str = DEFAULT_PROFILE,
234
+ ws_id: int | None = None,
235
+ ) -> dict:
236
+ return workspace.workspace_get(
237
+ profile=profile,
238
+ ws_id=ws_id,
239
+ )
240
+
231
241
  @server.tool()
232
242
  def app_list(profile: str = DEFAULT_PROFILE) -> dict:
233
243
  return apps.app_list(profile=profile)