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

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.84
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.84 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.84",
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.0b84"
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.0b84"
@@ -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(
@@ -875,8 +879,19 @@ class AiBuilderFacade:
875
879
  )
876
880
  current_items = raw_tag_items
877
881
 
882
+ current_group_specs = _collect_backend_package_group_specs(current_items)
883
+ normalized_items, group_resolution_issues = _align_public_package_group_ids(items, current_group_specs=current_group_specs)
884
+ if group_resolution_issues:
885
+ return _failed(
886
+ "PACKAGE_GROUP_AMBIGUOUS",
887
+ "items contains group names that match multiple existing package groups; pass explicit group_id to disambiguate",
888
+ normalized_args=normalized_args,
889
+ details={"package_id": package_id, "issues": group_resolution_issues},
890
+ suggested_next_call={"tool_name": "package_get", "arguments": {"profile": profile, "package_id": package_id}},
891
+ )
892
+
878
893
  current_resources = _flatten_package_resource_identities(current_items, public=False)
879
- desired_resources = _flatten_package_resource_identities(items, public=True)
894
+ desired_resources = _flatten_package_resource_identities(normalized_items, public=True)
880
895
  missing_resources = sorted(current_resources - desired_resources)
881
896
  if missing_resources and not allow_detach:
882
897
  return _failed(
@@ -887,7 +902,7 @@ class AiBuilderFacade:
887
902
  suggested_next_call=None,
888
903
  )
889
904
 
890
- duplicate_resources = _find_duplicate_package_resources(items)
905
+ duplicate_resources = _find_duplicate_package_resources(normalized_items)
891
906
  if duplicate_resources:
892
907
  return _failed(
893
908
  "PACKAGE_LAYOUT_DUPLICATE_ITEM",
@@ -897,8 +912,8 @@ class AiBuilderFacade:
897
912
  suggested_next_call=None,
898
913
  )
899
914
 
900
- current_groups = _collect_backend_package_groups(current_items)
901
- desired_groups = _collect_public_package_group_specs(items)
915
+ current_groups = {int(spec["group_id"]): str(spec["name"] or "").strip() for spec in current_group_specs}
916
+ desired_groups = _collect_public_package_group_specs(normalized_items)
902
917
  desired_group_ids = {
903
918
  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
919
  }
@@ -907,7 +922,7 @@ class AiBuilderFacade:
907
922
  permission_outcomes: list[PermissionCheckOutcome] = []
908
923
  needs_group_create = any(_coerce_positive_int(group.get("group_id")) is None for group in desired_groups)
909
924
  needs_group_delete = bool(deleted_group_ids)
910
- needs_edit_app = bool(items)
925
+ needs_edit_app = bool(normalized_items)
911
926
  for required_permission in (
912
927
  (["add_app"] if needs_group_create else [])
913
928
  + (["edit_app"] if needs_edit_app else [])
@@ -975,7 +990,7 @@ class AiBuilderFacade:
975
990
  group_ids_by_path[path] = group_id
976
991
 
977
992
  try:
978
- backend_items = _backend_package_items_from_public_items(items, group_ids_by_path)
993
+ backend_items = _backend_package_items_from_public_items(normalized_items, group_ids_by_path)
979
994
  except ValueError as error:
980
995
  return _failed(
981
996
  "PACKAGE_LAYOUT_INVALID",
@@ -984,8 +999,6 @@ class AiBuilderFacade:
984
999
  details={"package_id": package_id},
985
1000
  suggested_next_call=None,
986
1001
  )
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
1002
 
990
1003
  try:
991
1004
  sort_result = self.packages.package_sort_items(profile=profile, tag_id=package_id, tag_items=backend_items)
@@ -12419,22 +12432,36 @@ def _find_duplicate_package_resources(items: Any) -> list[tuple[str, str]]:
12419
12432
 
12420
12433
 
12421
12434
  def _collect_backend_package_groups(tag_items: Any) -> dict[int, str]:
12422
- groups: dict[int, str] = {}
12435
+ return {
12436
+ int(spec["group_id"]): str(spec["name"] or "").strip()
12437
+ for spec in _collect_backend_package_group_specs(tag_items)
12438
+ if _coerce_positive_int(spec.get("group_id")) is not None
12439
+ }
12423
12440
 
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
12441
 
12437
- walk(tag_items)
12442
+ def _collect_backend_package_group_specs(tag_items: Any, *, path: tuple[int, ...] = ()) -> list[JSONObject]:
12443
+ if not isinstance(tag_items, list):
12444
+ return []
12445
+ groups: list[JSONObject] = []
12446
+ for index, item in enumerate(tag_items):
12447
+ if not isinstance(item, dict):
12448
+ continue
12449
+ item_type = _coerce_positive_int(item.get("itemType"))
12450
+ child_path = (*path, index)
12451
+ if item_type == 3:
12452
+ group_id = _coerce_positive_int(item.get("groupId"))
12453
+ if group_id is None:
12454
+ continue
12455
+ child_items = item.get("subItems") if isinstance(item.get("subItems"), list) else []
12456
+ groups.append(
12457
+ {
12458
+ "path": list(child_path),
12459
+ "group_id": group_id,
12460
+ "name": str(item.get("title") or item.get("groupName") or "").strip(),
12461
+ "resource_signature": _package_resource_signature(child_items, public=False),
12462
+ }
12463
+ )
12464
+ groups.extend(_collect_backend_package_group_specs(child_items, path=child_path))
12438
12465
  return groups
12439
12466
 
12440
12467
 
@@ -12459,6 +12486,91 @@ def _collect_public_package_group_specs(items: Any, *, path: tuple[int, ...] = (
12459
12486
  return groups
12460
12487
 
12461
12488
 
12489
+ def _align_public_package_group_ids(
12490
+ items: list[dict[str, Any]],
12491
+ *,
12492
+ current_group_specs: list[JSONObject],
12493
+ ) -> tuple[list[dict[str, Any]], list[JSONObject]]:
12494
+ normalized_items = deepcopy(items)
12495
+ used_group_ids: set[int] = set()
12496
+ issues: list[JSONObject] = []
12497
+
12498
+ def walk(nodes: list[dict[str, Any]], *, path: tuple[int, ...] = ()) -> None:
12499
+ for index, node in enumerate(nodes):
12500
+ if not isinstance(node, dict):
12501
+ continue
12502
+ item_type = str(node.get("type") or "").strip().lower()
12503
+ child_path = (*path, index)
12504
+ child_items = node.get("items") if isinstance(node.get("items"), list) else []
12505
+ if item_type == "group" or isinstance(node.get("items"), list):
12506
+ explicit_group_id = _coerce_positive_int(node.get("group_id") or node.get("groupId"))
12507
+ if explicit_group_id is not None:
12508
+ node["group_id"] = explicit_group_id
12509
+ used_group_ids.add(explicit_group_id)
12510
+ else:
12511
+ group_name = str(node.get("name") or node.get("title") or node.get("group_name") or "").strip()
12512
+ matched_group, ambiguity = _match_existing_package_group(
12513
+ group_name=group_name,
12514
+ child_items=child_items,
12515
+ path=child_path,
12516
+ current_group_specs=current_group_specs,
12517
+ used_group_ids=used_group_ids,
12518
+ )
12519
+ if ambiguity is not None:
12520
+ issues.append(ambiguity)
12521
+ elif matched_group is not None:
12522
+ matched_group_id = _coerce_positive_int(matched_group.get("group_id"))
12523
+ if matched_group_id is not None:
12524
+ node["group_id"] = matched_group_id
12525
+ used_group_ids.add(matched_group_id)
12526
+ walk(child_items, path=child_path)
12527
+
12528
+ walk(normalized_items)
12529
+ return normalized_items, issues
12530
+
12531
+
12532
+ def _match_existing_package_group(
12533
+ *,
12534
+ group_name: str,
12535
+ child_items: list[dict[str, Any]],
12536
+ path: tuple[int, ...],
12537
+ current_group_specs: list[JSONObject],
12538
+ used_group_ids: set[int],
12539
+ ) -> tuple[JSONObject | None, JSONObject | None]:
12540
+ desired_signature = _package_resource_signature(child_items, public=True)
12541
+ candidates = [
12542
+ spec
12543
+ for spec in current_group_specs
12544
+ if _coerce_positive_int(spec.get("group_id")) is not None
12545
+ and int(spec["group_id"]) not in used_group_ids
12546
+ and str(spec.get("name") or "").strip() == group_name
12547
+ ]
12548
+ exact_matches = [spec for spec in candidates if spec.get("resource_signature") == desired_signature]
12549
+ if len(exact_matches) == 1:
12550
+ return exact_matches[0], None
12551
+ path_matches = [spec for spec in candidates if tuple(spec.get("path") or ()) == path]
12552
+ if len(path_matches) == 1:
12553
+ return path_matches[0], None
12554
+ if len(candidates) == 1:
12555
+ return candidates[0], None
12556
+ ambiguous_matches = exact_matches if len(exact_matches) > 1 else candidates if len(candidates) > 1 else []
12557
+ if ambiguous_matches:
12558
+ return None, {
12559
+ "path": list(path),
12560
+ "name": group_name,
12561
+ "candidate_group_ids": [
12562
+ int(spec["group_id"])
12563
+ for spec in ambiguous_matches
12564
+ if _coerce_positive_int(spec.get("group_id")) is not None
12565
+ ],
12566
+ }
12567
+ return None, None
12568
+
12569
+
12570
+ def _package_resource_signature(items: Any, *, public: bool) -> tuple[tuple[str, str], ...]:
12571
+ return tuple(sorted(_flatten_package_resource_identities(items, public=public)))
12572
+
12573
+
12462
12574
  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
12575
  backend_items: list[JSONObject] = []
12464
12576
  for index, item in enumerate(items):