@josephyan/qingflow-app-user-mcp 0.2.0-beta.982 → 0.2.0-beta.983

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.982
6
+ npm install @josephyan/qingflow-app-user-mcp@0.2.0-beta.983
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.982 qingflow-app-user-mcp
12
+ npx -y -p @josephyan/qingflow-app-user-mcp@0.2.0-beta.983 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.982",
3
+ "version": "0.2.0-beta.983",
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.0b982"
7
+ version = "0.2.0b983"
8
8
  description = "User-authenticated MCP server for Qingflow"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -5,7 +5,7 @@ from pathlib import Path
5
5
 
6
6
  __all__ = ["__version__"]
7
7
 
8
- _FALLBACK_VERSION = "0.2.0b982"
8
+ _FALLBACK_VERSION = "0.2.0b983"
9
9
 
10
10
 
11
11
  def _resolve_local_pyproject_version() -> str | None:
@@ -2786,6 +2786,19 @@ class AiBuilderFacade:
2786
2786
  "can_copy_app": _coerce_optional_bool(base.get("copyAppStatus")),
2787
2787
  }
2788
2788
 
2789
+ def _derive_can_edit_app_base(self, *, profile: str, permission_summary: JSONObject) -> bool:
2790
+ if permission_summary.get("can_edit_app") is not True:
2791
+ return False
2792
+ tag_ids = _coerce_int_list(permission_summary.get("tag_ids"))
2793
+ for tag_id in tag_ids:
2794
+ try:
2795
+ package_permission = self._read_package_permission_summary(profile=profile, tag_id=tag_id)
2796
+ except (QingflowApiError, RuntimeError):
2797
+ return False
2798
+ if package_permission.get("can_edit_tag") is not True:
2799
+ return False
2800
+ return True
2801
+
2789
2802
  def _read_portal_permission_summary(self, *, dash_key: str, portal_result: dict[str, Any]) -> JSONObject:
2790
2803
  tag_ids = _coerce_int_list(portal_result.get("tagIds"))
2791
2804
  if not tag_ids:
@@ -2999,7 +3012,7 @@ class AiBuilderFacade:
2999
3012
 
3000
3013
  def app_read_summary(self, *, profile: str, app_key: str) -> JSONObject:
3001
3014
  try:
3002
- state = self._load_base_schema_state(profile=profile, app_key=app_key)
3015
+ base = self.apps.app_get_base(profile=profile, app_key=app_key, include_raw=True)
3003
3016
  except (QingflowApiError, RuntimeError) as error:
3004
3017
  api_error = _coerce_api_error(error)
3005
3018
  return _failed_from_api_error(
@@ -3009,26 +3022,55 @@ class AiBuilderFacade:
3009
3022
  details={"app_key": app_key},
3010
3023
  suggested_next_call={"tool_name": "app_resolve", "arguments": {"profile": profile, "app_key": app_key}},
3011
3024
  )
3012
- views, views_unavailable = self._load_views_result(profile=profile, app_key=app_key, tolerate_404=True)
3013
- workflow, workflow_unavailable = self._load_workflow_result(profile=profile, app_key=app_key, tolerate_404=True)
3014
- parsed = state["parsed"]
3025
+ base_result = base.get("result") if isinstance(base.get("result"), dict) else {}
3026
+ schema_unavailable = False
3027
+ try:
3028
+ schema_result, _schema_source = self._read_schema_with_fallback(profile=profile, app_key=app_key)
3029
+ parsed = _parse_schema(schema_result)
3030
+ except (QingflowApiError, RuntimeError) as error:
3031
+ api_error = _coerce_api_error(error)
3032
+ if api_error.http_status == 404 or _is_permission_restricted_api_error(api_error):
3033
+ schema_unavailable = True
3034
+ parsed = {"fields": [], "layout": {"sections": []}}
3035
+ else:
3036
+ return _failed_from_api_error(
3037
+ "APP_READ_FAILED",
3038
+ api_error,
3039
+ normalized_args={"app_key": app_key},
3040
+ details={"app_key": app_key},
3041
+ suggested_next_call={"tool_name": "app_resolve", "arguments": {"profile": profile, "app_key": app_key}},
3042
+ )
3043
+ views, views_unavailable = self._load_views_result(
3044
+ profile=profile,
3045
+ app_key=app_key,
3046
+ tolerate_404=True,
3047
+ tolerate_permission_restricted=True,
3048
+ )
3049
+ workflow, workflow_unavailable = self._load_workflow_result(
3050
+ profile=profile,
3051
+ app_key=app_key,
3052
+ tolerate_404=True,
3053
+ tolerate_permission_restricted=True,
3054
+ )
3015
3055
  verification_hints = _build_verification_hints(
3016
- tag_ids=_coerce_int_list(state["base"].get("tagIds")),
3056
+ tag_ids=_coerce_int_list(base_result.get("tagIds")),
3017
3057
  fields=parsed["fields"],
3018
3058
  layout=parsed["layout"],
3019
3059
  views=_summarize_views(views),
3020
3060
  )
3061
+ if schema_unavailable:
3062
+ verification_hints.append("schema_read_unavailable")
3021
3063
  if views_unavailable:
3022
3064
  verification_hints.append("views_read_unavailable")
3023
3065
  if workflow_unavailable:
3024
3066
  verification_hints.append("workflow_read_unavailable")
3025
3067
  response = AppReadSummaryResponse(
3026
3068
  app_key=app_key,
3027
- title=state["base"].get("formTitle"),
3028
- app_icon=str(state["base"].get("appIcon") or "").strip() or None,
3029
- visibility=_public_visibility_from_member_auth(state["base"].get("auth")),
3030
- tag_ids=_coerce_int_list(state["base"].get("tagIds")),
3031
- publish_status=state["base"].get("appPublishStatus"),
3069
+ title=base_result.get("formTitle"),
3070
+ app_icon=str(base_result.get("appIcon") or "").strip() or None,
3071
+ visibility=_public_visibility_from_member_auth(base_result.get("auth")),
3072
+ tag_ids=_coerce_int_list(base_result.get("tagIds")),
3073
+ publish_status=base_result.get("appPublishStatus"),
3032
3074
  field_count=len(parsed["fields"]),
3033
3075
  layout_section_count=len(parsed["layout"].get("sections", [])),
3034
3076
  view_count=len(_summarize_views(views)),
@@ -3050,10 +3092,11 @@ class AiBuilderFacade:
3050
3092
  "warnings": _warnings_from_verification_hints(verification_hints),
3051
3093
  "verification": {
3052
3094
  "app_exists": True,
3095
+ "schema_read_unavailable": schema_unavailable,
3053
3096
  "views_read_unavailable": views_unavailable,
3054
3097
  "workflow_read_unavailable": workflow_unavailable,
3055
3098
  },
3056
- "verified": not views_unavailable and not workflow_unavailable,
3099
+ "verified": not schema_unavailable and not views_unavailable and not workflow_unavailable,
3057
3100
  **response.model_dump(mode="json"),
3058
3101
  }
3059
3102
 
@@ -3066,8 +3109,9 @@ class AiBuilderFacade:
3066
3109
  permission_summary = self._read_app_permission_summary(profile=profile, app_key=app_key)
3067
3110
  result["message"] = "read app config summary"
3068
3111
  result["editability"] = {
3112
+ "can_edit_app_base": self._derive_can_edit_app_base(profile=profile, permission_summary=permission_summary),
3069
3113
  "can_edit_form": permission_summary.get("can_edit_app"),
3070
- "can_edit_flow": permission_summary.get("can_edit_app"),
3114
+ "can_edit_flow": permission_summary.get("can_manage_data"),
3071
3115
  "can_edit_views": permission_summary.get("can_manage_data"),
3072
3116
  "can_edit_charts": permission_summary.get("can_manage_data"),
3073
3117
  }
@@ -7444,17 +7488,35 @@ class AiBuilderFacade:
7444
7488
  sync_result = {**sync_result, "button_config_restored": True}
7445
7489
  return sync_result
7446
7490
 
7447
- def _load_views_result(self, *, profile: str, app_key: str, tolerate_404: bool) -> tuple[Any, bool]:
7491
+ def _load_views_result(
7492
+ self,
7493
+ *,
7494
+ profile: str,
7495
+ app_key: str,
7496
+ tolerate_404: bool,
7497
+ tolerate_permission_restricted: bool = False,
7498
+ ) -> tuple[Any, bool]:
7448
7499
  try:
7449
7500
  views = self.views.view_list_flat(profile=profile, app_key=app_key)
7450
7501
  except (QingflowApiError, RuntimeError) as error:
7451
7502
  api_error = _coerce_api_error(error)
7452
- if api_error.http_status == 404:
7503
+ if api_error.http_status == 404 or (
7504
+ tolerate_permission_restricted and _is_permission_restricted_api_error(api_error)
7505
+ ):
7453
7506
  try:
7454
7507
  legacy_views = self.views.view_list(profile=profile, app_key=app_key)
7455
7508
  except (QingflowApiError, RuntimeError) as legacy_error:
7456
7509
  legacy_api_error = _coerce_api_error(legacy_error)
7457
- if tolerate_404 and legacy_api_error.http_status == 404:
7510
+ if (
7511
+ tolerate_404
7512
+ and (
7513
+ legacy_api_error.http_status == 404
7514
+ or (
7515
+ tolerate_permission_restricted
7516
+ and _is_permission_restricted_api_error(legacy_api_error)
7517
+ )
7518
+ )
7519
+ ):
7458
7520
  return [], True
7459
7521
  raise
7460
7522
  legacy_result = legacy_views.get("result")
@@ -7471,19 +7533,38 @@ class AiBuilderFacade:
7471
7533
  legacy_views = self.views.view_list(profile=profile, app_key=app_key)
7472
7534
  except (QingflowApiError, RuntimeError) as legacy_error:
7473
7535
  legacy_api_error = _coerce_api_error(legacy_error)
7474
- if tolerate_404 and legacy_api_error.http_status == 404:
7536
+ if (
7537
+ tolerate_404
7538
+ and (
7539
+ legacy_api_error.http_status == 404
7540
+ or (
7541
+ tolerate_permission_restricted
7542
+ and _is_permission_restricted_api_error(legacy_api_error)
7543
+ )
7544
+ )
7545
+ ):
7475
7546
  return normalized_views, False
7476
7547
  raise
7477
7548
  legacy_result = legacy_views.get("result")
7478
7549
  legacy_normalized = _normalize_view_collection(legacy_result)
7479
7550
  return legacy_normalized or normalized_views, False
7480
7551
 
7481
- def _load_workflow_result(self, *, profile: str, app_key: str, tolerate_404: bool) -> tuple[Any, bool]:
7552
+ def _load_workflow_result(
7553
+ self,
7554
+ *,
7555
+ profile: str,
7556
+ app_key: str,
7557
+ tolerate_404: bool,
7558
+ tolerate_permission_restricted: bool = False,
7559
+ ) -> tuple[Any, bool]:
7482
7560
  try:
7483
7561
  workflow = self.workflows.workflow_list_nodes(profile=profile, app_key=app_key)
7484
7562
  except (QingflowApiError, RuntimeError) as error:
7485
7563
  api_error = _coerce_api_error(error)
7486
- if tolerate_404 and api_error.http_status == 404:
7564
+ if tolerate_404 and (
7565
+ api_error.http_status == 404
7566
+ or (tolerate_permission_restricted and _is_permission_restricted_api_error(api_error))
7567
+ ):
7487
7568
  return [], True
7488
7569
  raise
7489
7570
  return workflow.get("result"), False
@@ -12241,6 +12322,7 @@ def _warnings_from_verification_hints(hints: list[str]) -> list[dict[str, Any]]:
12241
12322
  "package attachment not verified": _warning("PACKAGE_ATTACHMENT_UNVERIFIED", "package attachment is not verified"),
12242
12323
  "layout has unplaced fields": _warning("LAYOUT_HAS_UNPLACED_FIELDS", "layout still contains unplaced fields"),
12243
12324
  "no public views detected": _warning("NO_PUBLIC_VIEWS", "no public views were detected"),
12325
+ "schema_read_unavailable": _warning("SCHEMA_READ_UNAVAILABLE", "schema summary readback is unavailable"),
12244
12326
  "views_read_unavailable": _warning("VIEWS_READ_UNAVAILABLE", "views summary readback is unavailable"),
12245
12327
  "workflow_read_unavailable": _warning("WORKFLOW_READ_UNAVAILABLE", "workflow summary readback is unavailable"),
12246
12328
  }
@@ -118,6 +118,7 @@ def _format_app_get(result: dict[str, Any]) -> str:
118
118
  if editability:
119
119
  lines.append(
120
120
  "Editability: "
121
+ f"app_base={editability.get('can_edit_app_base')} / "
121
122
  f"form={editability.get('can_edit_form')} / "
122
123
  f"flow={editability.get('can_edit_flow')} / "
123
124
  f"views={editability.get('can_edit_views')} / "
@@ -3058,7 +3058,9 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
3058
3058
  "execution_notes": [
3059
3059
  "returns builder-side app configuration summary and editability",
3060
3060
  "use this as the default builder discovery read before fields/layout/views/flow/charts detail reads",
3061
- "editability reflects builder permissions, not end-user data visibility",
3061
+ "editability is route-aware builder capability summary, not end-user data visibility",
3062
+ "can_edit_app_base covers app base-info writes such as app_name, icon, and visibility",
3063
+ "can_edit_form covers form/schema routes only and does not imply app base-info writes",
3062
3064
  "returns normalized app visibility when backend auth is readable",
3063
3065
  ],
3064
3066
  "minimal_example": {