@josephyan/qingflow-app-user-mcp 0.2.0-beta.83 → 0.2.0-beta.85

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-user-mcp@0.2.0-beta.83
6
+ npm install @josephyan/qingflow-app-user-mcp@0.2.0-beta.85
7
7
  ```
8
8
 
9
9
  Run:
10
10
 
11
11
  ```bash
12
- npx -y -p @josephyan/qingflow-app-user-mcp@0.2.0-beta.83 qingflow-app-user-mcp
12
+ npx -y -p @josephyan/qingflow-app-user-mcp@0.2.0-beta.85 qingflow-app-user-mcp
13
13
  ```
14
14
 
15
15
  Environment:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@josephyan/qingflow-app-user-mcp",
3
- "version": "0.2.0-beta.83",
3
+ "version": "0.2.0-beta.85",
4
4
  "description": "Operational end-user MCP for Qingflow records, tasks, comments, and directory 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.0b83"
7
+ version = "0.2.0b85"
8
8
  description = "User-authenticated MCP server for Qingflow"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -2,4 +2,4 @@ from __future__ import annotations
2
2
 
3
3
  __all__ = ["__version__"]
4
4
 
5
- __version__ = "0.2.0b83"
5
+ __version__ = "0.2.0b85"
@@ -450,6 +450,10 @@ class AiBuilderFacade:
450
450
  "http_status": detail_read_error.http_status,
451
451
  }
452
452
  )
453
+ public_items = _public_package_items_from_tag_items(source.get("tagItems") or base.get("tagItems"))
454
+ item_count = summary.get("itemCount")
455
+ if not isinstance(item_count, int) or item_count < 0 or (item_count == 0 and public_items):
456
+ item_count = len(public_items)
453
457
  return {
454
458
  "status": "success",
455
459
  "error_code": None,
@@ -469,13 +473,13 @@ class AiBuilderFacade:
469
473
  "package_name": str(source.get("tagName") or base.get("tagName") or "").strip() or None,
470
474
  "icon": str(source.get("tagIcon") or base.get("tagIcon") or "").strip() or None,
471
475
  "publish_status": source.get("publishStatus") if source.get("publishStatus") is not None else base.get("publishStatus"),
472
- "item_count": summary.get("itemCount"),
476
+ "item_count": item_count,
473
477
  "add_app_status": base.get("addAppStatus") if base.get("addAppStatus") is not None else summary.get("addAppStatus"),
474
478
  "edit_app_status": base.get("editAppStatus") if base.get("editAppStatus") is not None else summary.get("editAppStatus"),
475
479
  "del_app_status": base.get("delAppStatus") if base.get("delAppStatus") is not None else summary.get("delAppStatus"),
476
480
  "edit_tag_status": base.get("editTagStatus") if base.get("editTagStatus") is not None else summary.get("editTagStatus"),
477
481
  "visibility": _public_visibility_from_member_auth(base.get("auth") or source.get("auth")),
478
- "items": _public_package_items_from_tag_items(source.get("tagItems") or base.get("tagItems")),
482
+ "items": public_items,
479
483
  }
480
484
 
481
485
  def package_apply(
@@ -844,15 +848,18 @@ class AiBuilderFacade:
844
848
  allow_detach: bool,
845
849
  normalized_args: JSONObject,
846
850
  ) -> JSONObject:
847
- current_result: JSONObject | None = None
851
+ current_detail_result: JSONObject | None = None
852
+ current_base_result: JSONObject | None = None
853
+ detail_api_error: QingflowApiError | None = None
848
854
  try:
849
- current_result = self.packages.package_get(profile=profile, tag_id=package_id, include_raw=True)
855
+ current_detail_result = self.packages.package_get(profile=profile, tag_id=package_id, include_raw=True)
850
856
  except (QingflowApiError, RuntimeError) as detail_error:
851
857
  detail_api_error = _coerce_api_error(detail_error)
852
- try:
853
- current_result = self.packages.package_get_base(profile=profile, tag_id=package_id, include_raw=True)
854
- except (QingflowApiError, RuntimeError) as base_error:
855
- api_error = _coerce_api_error(base_error)
858
+ try:
859
+ current_base_result = self.packages.package_get_base(profile=profile, tag_id=package_id, include_raw=True)
860
+ except (QingflowApiError, RuntimeError) as base_error:
861
+ api_error = _coerce_api_error(base_error)
862
+ if current_detail_result is None:
856
863
  return _failed_from_api_error(
857
864
  "PACKAGE_LAYOUT_READ_FAILED",
858
865
  api_error,
@@ -863,8 +870,20 @@ class AiBuilderFacade:
863
870
  },
864
871
  suggested_next_call={"tool_name": "package_get", "arguments": {"profile": profile, "package_id": package_id}},
865
872
  )
866
- current_raw = current_result.get("result") if isinstance(current_result.get("result"), dict) else {}
867
- raw_tag_items = current_raw.get("tagItems")
873
+
874
+ detail_raw = (
875
+ current_detail_result.get("result")
876
+ if isinstance(current_detail_result, dict) and isinstance(current_detail_result.get("result"), dict)
877
+ else {}
878
+ )
879
+ base_raw = (
880
+ current_base_result.get("result")
881
+ if isinstance(current_base_result, dict) and isinstance(current_base_result.get("result"), dict)
882
+ else {}
883
+ )
884
+ detail_tag_items = detail_raw.get("tagItems") if isinstance(detail_raw.get("tagItems"), list) else None
885
+ base_tag_items = base_raw.get("tagItems") if isinstance(base_raw.get("tagItems"), list) else None
886
+ raw_tag_items = detail_tag_items if detail_tag_items else base_tag_items
868
887
  if not isinstance(raw_tag_items, list):
869
888
  return _failed(
870
889
  "PACKAGE_LAYOUT_UNREADABLE",
@@ -875,8 +894,19 @@ class AiBuilderFacade:
875
894
  )
876
895
  current_items = raw_tag_items
877
896
 
897
+ current_group_specs = _collect_backend_package_group_specs(current_items)
898
+ normalized_items, group_resolution_issues = _align_public_package_group_ids(items, current_group_specs=current_group_specs)
899
+ if group_resolution_issues:
900
+ return _failed(
901
+ "PACKAGE_GROUP_AMBIGUOUS",
902
+ "items contains group names that match multiple existing package groups; pass explicit group_id to disambiguate",
903
+ normalized_args=normalized_args,
904
+ details={"package_id": package_id, "issues": group_resolution_issues},
905
+ suggested_next_call={"tool_name": "package_get", "arguments": {"profile": profile, "package_id": package_id}},
906
+ )
907
+
878
908
  current_resources = _flatten_package_resource_identities(current_items, public=False)
879
- desired_resources = _flatten_package_resource_identities(items, public=True)
909
+ desired_resources = _flatten_package_resource_identities(normalized_items, public=True)
880
910
  missing_resources = sorted(current_resources - desired_resources)
881
911
  if missing_resources and not allow_detach:
882
912
  return _failed(
@@ -887,7 +917,7 @@ class AiBuilderFacade:
887
917
  suggested_next_call=None,
888
918
  )
889
919
 
890
- duplicate_resources = _find_duplicate_package_resources(items)
920
+ duplicate_resources = _find_duplicate_package_resources(normalized_items)
891
921
  if duplicate_resources:
892
922
  return _failed(
893
923
  "PACKAGE_LAYOUT_DUPLICATE_ITEM",
@@ -897,8 +927,8 @@ class AiBuilderFacade:
897
927
  suggested_next_call=None,
898
928
  )
899
929
 
900
- current_groups = _collect_backend_package_groups(current_items)
901
- desired_groups = _collect_public_package_group_specs(items)
930
+ current_groups = {int(spec["group_id"]): str(spec["name"] or "").strip() for spec in current_group_specs}
931
+ desired_groups = _collect_public_package_group_specs(normalized_items)
902
932
  desired_group_ids = {
903
933
  group_id for group_id in (_coerce_positive_int(group.get("group_id")) for group in desired_groups) if group_id is not None
904
934
  }
@@ -907,7 +937,7 @@ class AiBuilderFacade:
907
937
  permission_outcomes: list[PermissionCheckOutcome] = []
908
938
  needs_group_create = any(_coerce_positive_int(group.get("group_id")) is None for group in desired_groups)
909
939
  needs_group_delete = bool(deleted_group_ids)
910
- needs_edit_app = bool(items)
940
+ needs_edit_app = bool(normalized_items)
911
941
  for required_permission in (
912
942
  (["add_app"] if needs_group_create else [])
913
943
  + (["edit_app"] if needs_edit_app else [])
@@ -975,7 +1005,7 @@ class AiBuilderFacade:
975
1005
  group_ids_by_path[path] = group_id
976
1006
 
977
1007
  try:
978
- backend_items = _backend_package_items_from_public_items(items, group_ids_by_path)
1008
+ backend_items = _backend_package_items_from_public_items(normalized_items, group_ids_by_path)
979
1009
  except ValueError as error:
980
1010
  return _failed(
981
1011
  "PACKAGE_LAYOUT_INVALID",
@@ -984,8 +1014,6 @@ class AiBuilderFacade:
984
1014
  details={"package_id": package_id},
985
1015
  suggested_next_call=None,
986
1016
  )
987
- for group_id in deleted_group_ids:
988
- backend_items.append({"itemType": 3, "groupId": group_id, "title": current_groups.get(group_id) or "", "subItems": []})
989
1017
 
990
1018
  try:
991
1019
  sort_result = self.packages.package_sort_items(profile=profile, tag_id=package_id, tag_items=backend_items)
@@ -12419,22 +12447,36 @@ def _find_duplicate_package_resources(items: Any) -> list[tuple[str, str]]:
12419
12447
 
12420
12448
 
12421
12449
  def _collect_backend_package_groups(tag_items: Any) -> dict[int, str]:
12422
- groups: dict[int, str] = {}
12450
+ return {
12451
+ int(spec["group_id"]): str(spec["name"] or "").strip()
12452
+ for spec in _collect_backend_package_group_specs(tag_items)
12453
+ if _coerce_positive_int(spec.get("group_id")) is not None
12454
+ }
12423
12455
 
12424
- def walk(value: Any) -> None:
12425
- if isinstance(value, list):
12426
- for child in value:
12427
- walk(child)
12428
- return
12429
- if not isinstance(value, dict):
12430
- return
12431
- if _coerce_positive_int(value.get("itemType")) == 3:
12432
- group_id = _coerce_positive_int(value.get("groupId"))
12433
- if group_id is not None:
12434
- groups[group_id] = str(value.get("title") or value.get("groupName") or "").strip()
12435
- walk(value.get("subItems"))
12436
12456
 
12437
- walk(tag_items)
12457
+ def _collect_backend_package_group_specs(tag_items: Any, *, path: tuple[int, ...] = ()) -> list[JSONObject]:
12458
+ if not isinstance(tag_items, list):
12459
+ return []
12460
+ groups: list[JSONObject] = []
12461
+ for index, item in enumerate(tag_items):
12462
+ if not isinstance(item, dict):
12463
+ continue
12464
+ item_type = _coerce_positive_int(item.get("itemType"))
12465
+ child_path = (*path, index)
12466
+ if item_type == 3:
12467
+ group_id = _coerce_positive_int(item.get("groupId"))
12468
+ if group_id is None:
12469
+ continue
12470
+ child_items = item.get("subItems") if isinstance(item.get("subItems"), list) else []
12471
+ groups.append(
12472
+ {
12473
+ "path": list(child_path),
12474
+ "group_id": group_id,
12475
+ "name": str(item.get("title") or item.get("groupName") or "").strip(),
12476
+ "resource_signature": _package_resource_signature(child_items, public=False),
12477
+ }
12478
+ )
12479
+ groups.extend(_collect_backend_package_group_specs(child_items, path=child_path))
12438
12480
  return groups
12439
12481
 
12440
12482
 
@@ -12459,6 +12501,91 @@ def _collect_public_package_group_specs(items: Any, *, path: tuple[int, ...] = (
12459
12501
  return groups
12460
12502
 
12461
12503
 
12504
+ def _align_public_package_group_ids(
12505
+ items: list[dict[str, Any]],
12506
+ *,
12507
+ current_group_specs: list[JSONObject],
12508
+ ) -> tuple[list[dict[str, Any]], list[JSONObject]]:
12509
+ normalized_items = deepcopy(items)
12510
+ used_group_ids: set[int] = set()
12511
+ issues: list[JSONObject] = []
12512
+
12513
+ def walk(nodes: list[dict[str, Any]], *, path: tuple[int, ...] = ()) -> None:
12514
+ for index, node in enumerate(nodes):
12515
+ if not isinstance(node, dict):
12516
+ continue
12517
+ item_type = str(node.get("type") or "").strip().lower()
12518
+ child_path = (*path, index)
12519
+ child_items = node.get("items") if isinstance(node.get("items"), list) else []
12520
+ if item_type == "group" or isinstance(node.get("items"), list):
12521
+ explicit_group_id = _coerce_positive_int(node.get("group_id") or node.get("groupId"))
12522
+ if explicit_group_id is not None:
12523
+ node["group_id"] = explicit_group_id
12524
+ used_group_ids.add(explicit_group_id)
12525
+ else:
12526
+ group_name = str(node.get("name") or node.get("title") or node.get("group_name") or "").strip()
12527
+ matched_group, ambiguity = _match_existing_package_group(
12528
+ group_name=group_name,
12529
+ child_items=child_items,
12530
+ path=child_path,
12531
+ current_group_specs=current_group_specs,
12532
+ used_group_ids=used_group_ids,
12533
+ )
12534
+ if ambiguity is not None:
12535
+ issues.append(ambiguity)
12536
+ elif matched_group is not None:
12537
+ matched_group_id = _coerce_positive_int(matched_group.get("group_id"))
12538
+ if matched_group_id is not None:
12539
+ node["group_id"] = matched_group_id
12540
+ used_group_ids.add(matched_group_id)
12541
+ walk(child_items, path=child_path)
12542
+
12543
+ walk(normalized_items)
12544
+ return normalized_items, issues
12545
+
12546
+
12547
+ def _match_existing_package_group(
12548
+ *,
12549
+ group_name: str,
12550
+ child_items: list[dict[str, Any]],
12551
+ path: tuple[int, ...],
12552
+ current_group_specs: list[JSONObject],
12553
+ used_group_ids: set[int],
12554
+ ) -> tuple[JSONObject | None, JSONObject | None]:
12555
+ desired_signature = _package_resource_signature(child_items, public=True)
12556
+ candidates = [
12557
+ spec
12558
+ for spec in current_group_specs
12559
+ if _coerce_positive_int(spec.get("group_id")) is not None
12560
+ and int(spec["group_id"]) not in used_group_ids
12561
+ and str(spec.get("name") or "").strip() == group_name
12562
+ ]
12563
+ exact_matches = [spec for spec in candidates if spec.get("resource_signature") == desired_signature]
12564
+ if len(exact_matches) == 1:
12565
+ return exact_matches[0], None
12566
+ path_matches = [spec for spec in candidates if tuple(spec.get("path") or ()) == path]
12567
+ if len(path_matches) == 1:
12568
+ return path_matches[0], None
12569
+ if len(candidates) == 1:
12570
+ return candidates[0], None
12571
+ ambiguous_matches = exact_matches if len(exact_matches) > 1 else candidates if len(candidates) > 1 else []
12572
+ if ambiguous_matches:
12573
+ return None, {
12574
+ "path": list(path),
12575
+ "name": group_name,
12576
+ "candidate_group_ids": [
12577
+ int(spec["group_id"])
12578
+ for spec in ambiguous_matches
12579
+ if _coerce_positive_int(spec.get("group_id")) is not None
12580
+ ],
12581
+ }
12582
+ return None, None
12583
+
12584
+
12585
+ def _package_resource_signature(items: Any, *, public: bool) -> tuple[tuple[str, str], ...]:
12586
+ return tuple(sorted(_flatten_package_resource_identities(items, public=public)))
12587
+
12588
+
12462
12589
  def _backend_package_items_from_public_items(items: list[dict[str, Any]], group_ids_by_path: dict[tuple[int, ...], int], *, path: tuple[int, ...] = ()) -> list[JSONObject]:
12463
12590
  backend_items: list[JSONObject] = []
12464
12591
  for index, item in enumerate(items):