@josephyan/qingflow-app-user-mcp 1.1.19 → 1.1.21

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@1.1.19
6
+ npm install @josephyan/qingflow-app-user-mcp@1.1.21
7
7
  ```
8
8
 
9
9
  Run:
10
10
 
11
11
  ```bash
12
- npx -y -p @josephyan/qingflow-app-user-mcp@1.1.19 qingflow-app-user-mcp
12
+ npx -y -p @josephyan/qingflow-app-user-mcp@1.1.21 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": "1.1.19",
3
+ "version": "1.1.21",
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 = "1.1.19"
7
+ version = "1.1.21"
8
8
  description = "User-authenticated MCP server for Qingflow"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -9,6 +9,7 @@ import random
9
9
  import re
10
10
  import string
11
11
  import tempfile
12
+ import time
12
13
  from typing import Any, NoReturn, cast
13
14
  from urllib.parse import quote_plus, unquote_plus
14
15
  from uuid import uuid4
@@ -8585,7 +8586,17 @@ class AiBuilderFacade:
8585
8586
  add_fields: list[FieldPatch],
8586
8587
  update_fields: list[FieldUpdatePatch],
8587
8588
  remove_fields: list[FieldRemovePatch],
8589
+ inline_form_layout_sections: list[LayoutSectionPatch] | None = None,
8588
8590
  ) -> JSONObject:
8591
+ apply_started_at = time.perf_counter()
8592
+ schema_write_ms: int | None = None
8593
+ inline_layout_ms: int | None = None
8594
+ publish_ms: int | None = None
8595
+ readback_ms: int | None = None
8596
+ requested_inline_layout_sections = [
8597
+ section.model_dump(mode="json", exclude_none=True)
8598
+ for section in (inline_form_layout_sections or [])
8599
+ ]
8589
8600
  normalized_args = {
8590
8601
  "app_key": app_key,
8591
8602
  "package_tag_id": package_tag_id,
@@ -8599,6 +8610,8 @@ class AiBuilderFacade:
8599
8610
  "update_fields": [patch.model_dump(mode="json") for patch in update_fields],
8600
8611
  "remove_fields": [patch.model_dump(mode="json") for patch in remove_fields],
8601
8612
  }
8613
+ if requested_inline_layout_sections:
8614
+ normalized_args["inline_form_layout_sections"] = requested_inline_layout_sections
8602
8615
  try:
8603
8616
  desired_auth = (
8604
8617
  self._compile_visibility_to_member_auth(profile=profile, visibility=visibility)
@@ -8614,7 +8627,8 @@ class AiBuilderFacade:
8614
8627
  suggested_next_call=None,
8615
8628
  )
8616
8629
  permission_outcomes: list[PermissionCheckOutcome] = []
8617
- requested_field_changes = bool(add_fields or update_fields or remove_fields)
8630
+ requested_inline_layout = bool(requested_inline_layout_sections)
8631
+ requested_field_changes = bool(add_fields or update_fields or remove_fields or requested_inline_layout)
8618
8632
  resolved: JSONObject
8619
8633
  if app_key:
8620
8634
  resolved = self.app_resolve(profile=profile, app_key=app_key)
@@ -8950,6 +8964,59 @@ class AiBuilderFacade:
8950
8964
  suggested_next_call={"tool_name": "app_get_fields", "arguments": {"profile": profile, "app_key": target.app_key}},
8951
8965
  )
8952
8966
 
8967
+ inline_form_layout_target: dict[str, Any] | None = None
8968
+ inline_form_layout_auto_added_fields: list[str] = []
8969
+ inline_form_layout_changed = False
8970
+ if requested_inline_layout:
8971
+ inline_layout_started_at = time.perf_counter()
8972
+ resolved_inline_sections, missing_selectors = _resolve_layout_sections_to_names(
8973
+ requested_inline_layout_sections,
8974
+ current_fields,
8975
+ )
8976
+ normalized_args["inline_form_layout_sections"] = resolved_inline_sections
8977
+ if missing_selectors:
8978
+ return _failed(
8979
+ "UNKNOWN_LAYOUT_FIELD",
8980
+ "inline form layout references unknown field selectors",
8981
+ normalized_args=normalized_args,
8982
+ details={"unknown_selectors": missing_selectors},
8983
+ missing_fields=[str(item) for item in missing_selectors],
8984
+ suggested_next_call={"tool_name": "app_get_fields", "arguments": {"profile": profile, "app_key": target.app_key}},
8985
+ )
8986
+ fields_by_name = {field["name"]: field for field in current_fields}
8987
+ seen_layout_fields: list[str] = []
8988
+ for section in resolved_inline_sections:
8989
+ for row in section.get("rows", []):
8990
+ for field_name in row:
8991
+ if field_name not in fields_by_name:
8992
+ return _failed(
8993
+ "UNKNOWN_LAYOUT_FIELD",
8994
+ f"inline form layout references unknown field '{field_name}'",
8995
+ normalized_args=normalized_args,
8996
+ details={"field_name": field_name},
8997
+ suggested_next_call={"tool_name": "app_get_fields", "arguments": {"profile": profile, "app_key": target.app_key}},
8998
+ )
8999
+ if field_name in seen_layout_fields:
9000
+ return _failed(
9001
+ "DUPLICATE_LAYOUT_FIELD",
9002
+ f"inline form layout references field '{field_name}' more than once",
9003
+ normalized_args=normalized_args,
9004
+ details={"field_name": field_name},
9005
+ suggested_next_call={"tool_name": "app_schema_apply", "arguments": {"profile": profile, **normalized_args}},
9006
+ )
9007
+ seen_layout_fields.append(field_name)
9008
+ merged_inline_layout = _merge_layout(
9009
+ current_layout=layout,
9010
+ requested_sections=resolved_inline_sections,
9011
+ all_field_names=[field["name"] for field in current_fields],
9012
+ )
9013
+ inline_form_layout_auto_added_fields = list(merged_inline_layout.get("auto_added_fields") or [])
9014
+ inline_form_layout_target = cast(dict[str, Any], merged_inline_layout["layout"])
9015
+ inline_form_layout_changed = not _layouts_equal(layout, inline_form_layout_target)
9016
+ if inline_form_layout_changed:
9017
+ layout = inline_form_layout_target
9018
+ inline_layout_ms = _duration_ms(inline_layout_started_at)
9019
+
8953
9020
  schema_write_requested = bool(
8954
9021
  added
8955
9022
  or updated
@@ -8957,6 +9024,7 @@ class AiBuilderFacade:
8957
9024
  or normalized_code_block_fields
8958
9025
  or data_display_selection.has_any
8959
9026
  or bool(resolved.get("created"))
9027
+ or inline_form_layout_changed
8960
9028
  )
8961
9029
  if schema_write_requested:
8962
9030
  try:
@@ -9006,7 +9074,8 @@ class AiBuilderFacade:
9006
9074
  else []
9007
9075
  )
9008
9076
 
9009
- if not added and not updated and not removed and not normalized_code_block_fields and not data_display_selection.has_any and not bool(resolved.get("created")):
9077
+ if not schema_write_requested:
9078
+ readback_started_at = time.perf_counter()
9010
9079
  try:
9011
9080
  base_info = self.apps.app_get_base(profile=profile, app_key=target.app_key, include_raw=True).get("result") or {}
9012
9081
  except (QingflowApiError, RuntimeError) as error:
@@ -9042,6 +9111,7 @@ class AiBuilderFacade:
9042
9111
  expected_app_icon = str(visual_result.get("app_icon") or "").strip() or None
9043
9112
  app_icon_verified = True if expected_app_icon is None else actual_app_icon == expected_app_icon
9044
9113
  app_base_verified = actual_app_name == effective_app_name and app_icon_verified
9114
+ readback_ms = _duration_ms(readback_started_at)
9045
9115
  verified = app_base_verified and relation_target_metadata_verified
9046
9116
  response = {
9047
9117
  "status": "success" if verified else "partial_success",
@@ -9079,11 +9149,34 @@ class AiBuilderFacade:
9079
9149
  "package_attached": package_attached,
9080
9150
  }
9081
9151
  response["details"]["relation_field_count"] = relation_field_count
9152
+ if requested_inline_layout:
9153
+ response["inline_form_layout"] = {
9154
+ "requested": True,
9155
+ "section_count": len(requested_inline_layout_sections),
9156
+ "app_key": target.app_key,
9157
+ "applied": True,
9158
+ "applied_in_schema": True,
9159
+ "layout_status": "success",
9160
+ "message": "layout already matched requested state",
9161
+ "auto_added_fields": inline_form_layout_auto_added_fields,
9162
+ }
9163
+ response["verification"]["inline_form_layout_verified"] = True
9082
9164
  if normalized_code_block_fields:
9083
9165
  response["normalized_code_block_output_assignment"] = True
9084
9166
  response["normalized_code_block_fields"] = normalized_code_block_fields
9085
9167
  response = _apply_permission_outcomes(response, relation_permission_outcome)
9086
- return finalize(self._append_publish_result(profile=profile, app_key=target.app_key, publish=publish, response=response))
9168
+ publish_started_at = time.perf_counter()
9169
+ response = self._append_publish_result(profile=profile, app_key=target.app_key, publish=publish, response=response)
9170
+ publish_ms = _duration_ms(publish_started_at)
9171
+ _merge_duration_breakdown(
9172
+ response,
9173
+ schema_write_ms=0,
9174
+ inline_layout_ms=inline_layout_ms,
9175
+ publish_ms=publish_ms,
9176
+ readback_ms=readback_ms,
9177
+ total_ms=_duration_ms(apply_started_at),
9178
+ )
9179
+ return finalize(response)
9087
9180
 
9088
9181
  payload = (
9089
9182
  _build_form_payload_from_fields(
@@ -9109,7 +9202,9 @@ class AiBuilderFacade:
9109
9202
  current_schema=schema_result,
9110
9203
  )
9111
9204
  try:
9205
+ schema_write_started_at = time.perf_counter()
9112
9206
  self.apps.app_update_form_schema(profile=profile, app_key=target.app_key, payload=payload)
9207
+ schema_write_ms = _duration_ms(schema_write_started_at)
9113
9208
  except (QingflowApiError, RuntimeError) as error:
9114
9209
  api_error = _coerce_api_error(error)
9115
9210
  if backend_code_int(api_error) == 49614:
@@ -9271,7 +9366,9 @@ class AiBuilderFacade:
9271
9366
  current_schema=rebound_schema,
9272
9367
  )
9273
9368
  try:
9369
+ schema_write_started_at = time.perf_counter()
9274
9370
  self.apps.app_update_form_schema(profile=profile, app_key=target.app_key, payload=rebound_payload)
9371
+ schema_write_ms = (schema_write_ms or 0) + _duration_ms(schema_write_started_at)
9275
9372
  except (QingflowApiError, RuntimeError) as error:
9276
9373
  api_error = _coerce_api_error(error)
9277
9374
  return _failed_from_api_error(
@@ -9334,6 +9431,22 @@ class AiBuilderFacade:
9334
9431
  "package_attached": None,
9335
9432
  }
9336
9433
  response["details"]["relation_field_count"] = relation_field_count
9434
+ if requested_inline_layout:
9435
+ response["inline_form_layout"] = {
9436
+ "requested": True,
9437
+ "section_count": len(requested_inline_layout_sections),
9438
+ "app_key": target.app_key,
9439
+ "applied": True,
9440
+ "applied_in_schema": True,
9441
+ "layout_status": "success",
9442
+ "message": (
9443
+ "applied inline form layout inside schema write"
9444
+ if inline_form_layout_changed
9445
+ else "layout already matched requested state"
9446
+ ),
9447
+ "auto_added_fields": inline_form_layout_auto_added_fields,
9448
+ }
9449
+ response["verification"]["inline_form_layout_verified"] = False
9337
9450
  if normalized_code_block_fields:
9338
9451
  response["normalized_code_block_output_assignment"] = True
9339
9452
  response["normalized_code_block_fields"] = normalized_code_block_fields
@@ -9342,11 +9455,14 @@ class AiBuilderFacade:
9342
9455
  if schema_readback_delayed_error is not None:
9343
9456
  response["details"]["schema_readback_delayed_error"] = schema_readback_delayed_error
9344
9457
  response = _apply_permission_outcomes(response, relation_permission_outcome)
9458
+ publish_started_at = time.perf_counter()
9345
9459
  response = self._append_publish_result(profile=profile, app_key=target.app_key, publish=publish, response=response)
9460
+ publish_ms = _duration_ms(publish_started_at)
9346
9461
  verification_ok = False
9347
9462
  tag_ids_after: list[int] = []
9348
9463
  package_attached: bool | None = None
9349
9464
  verification_error: QingflowApiError | None = None
9465
+ readback_started_at = time.perf_counter()
9350
9466
  try:
9351
9467
  verified = self.app_read(profile=profile, app_key=target.app_key, include_raw=False)
9352
9468
  verified_field_names = {field["name"] for field in verified["schema"]["fields"]}
@@ -9380,6 +9496,14 @@ class AiBuilderFacade:
9380
9496
  response["verification"].update(data_display_verification)
9381
9497
  if data_display_verification.get("data_display_config_verified") is False:
9382
9498
  verification_ok = False
9499
+ if requested_inline_layout and inline_form_layout_target is not None:
9500
+ verified_layout = verified.get("layout") if isinstance(verified.get("layout"), dict) else {}
9501
+ inline_layout_verified = _layouts_equal(verified_layout, inline_form_layout_target) or _layouts_semantically_equal(
9502
+ verified_layout,
9503
+ inline_form_layout_target,
9504
+ )
9505
+ response["verification"]["inline_form_layout_verified"] = inline_layout_verified
9506
+ verification_ok = verification_ok and inline_layout_verified
9383
9507
  if relation_degraded_expectations:
9384
9508
  relation_verified_fields = cast(list[dict[str, Any]], verified["schema"]["fields"])
9385
9509
  try:
@@ -9412,6 +9536,7 @@ class AiBuilderFacade:
9412
9536
  tag_ids_after = []
9413
9537
  package_attached = None if package_tag_id is None else False
9414
9538
  app_base_verified = False
9539
+ readback_ms = _duration_ms(readback_started_at)
9415
9540
  response["verification"]["fields_verified"] = verification_ok
9416
9541
  response["verification"]["package_attached"] = package_attached
9417
9542
  response["verification"]["app_visuals_verified"] = app_base_verified
@@ -9460,6 +9585,14 @@ class AiBuilderFacade:
9460
9585
  "message": verification_error.message,
9461
9586
  **_transport_error_payload(verification_error),
9462
9587
  }
9588
+ _merge_duration_breakdown(
9589
+ response,
9590
+ schema_write_ms=schema_write_ms,
9591
+ inline_layout_ms=inline_layout_ms,
9592
+ publish_ms=publish_ms,
9593
+ readback_ms=readback_ms,
9594
+ total_ms=_duration_ms(apply_started_at),
9595
+ )
9463
9596
  return finalize(response)
9464
9597
 
9465
9598
  def app_layout_apply(
@@ -21636,6 +21769,21 @@ def _layouts_semantically_equal(left: dict[str, Any], right: dict[str, Any]) ->
21636
21769
  return _layout_field_sequence(left) == _layout_field_sequence(right)
21637
21770
 
21638
21771
 
21772
+ def _duration_ms(started_at: float) -> int:
21773
+ return max(0, int((time.perf_counter() - started_at) * 1000))
21774
+
21775
+
21776
+ def _merge_duration_breakdown(response: JSONObject, **items: int | None) -> None:
21777
+ breakdown = response.get("duration_breakdown_ms")
21778
+ if not isinstance(breakdown, dict):
21779
+ breakdown = {}
21780
+ response["duration_breakdown_ms"] = breakdown
21781
+ for key, value in items.items():
21782
+ if value is None:
21783
+ continue
21784
+ breakdown[key] = int(value)
21785
+
21786
+
21639
21787
  def _find_field_section_id(layout: dict[str, Any], field_name: str) -> str | None:
21640
21788
  for section in layout.get("sections", []) or []:
21641
21789
  for row in section.get("rows", []) or []:
@@ -107,6 +107,26 @@ def _payload_bool(payload: JSONObject, *keys: str, default: bool = True) -> bool
107
107
  return bool(value)
108
108
 
109
109
 
110
+ def _elapsed_ms(started_at: float) -> int:
111
+ return max(0, int((time.perf_counter() - started_at) * 1000))
112
+
113
+
114
+ def _merge_duration_breakdown(payload: JSONObject, **items: int | None) -> None:
115
+ breakdown = payload.get("duration_breakdown_ms")
116
+ if not isinstance(breakdown, dict):
117
+ breakdown = {}
118
+ payload["duration_breakdown_ms"] = breakdown
119
+ for key, value in items.items():
120
+ if value is None:
121
+ continue
122
+ breakdown[key] = int(value)
123
+
124
+
125
+ def _inline_form_layout_was_handled(payload: JSONObject) -> bool:
126
+ inline_layout = payload.get("inline_form_layout")
127
+ return isinstance(inline_layout, dict) and bool(inline_layout.get("requested"))
128
+
129
+
110
130
  PUBLIC_STABLE_FLOW_NODE_TYPES = ["start", "approve", "fill", "copy", "webhook", "end"]
111
131
  BUILDER_APPLY_SCHEMA_VERSION = "builder.apply.v1"
112
132
  BUILDER_APPLY_TOOL_NAMES = {
@@ -2359,6 +2379,7 @@ class AiBuilderTools(ToolBase):
2359
2379
  continue
2360
2380
 
2361
2381
  initial_add_fields, deferred_add_fields = _split_multi_app_initial_add_fields(item, is_new_app=not bool(app_key))
2382
+ inline_layout_sections = list(item.get("_inline_form_layout_sections") or [])
2362
2383
  item["_deferred_add_fields"] = deferred_add_fields
2363
2384
  shell = self._app_schema_apply_once(
2364
2385
  profile=profile,
@@ -2374,6 +2395,7 @@ class AiBuilderTools(ToolBase):
2374
2395
  add_fields=initial_add_fields,
2375
2396
  update_fields=[],
2376
2397
  remove_fields=[],
2398
+ inline_form_layout_sections=inline_layout_sections if not deferred_add_fields else None,
2377
2399
  )
2378
2400
  public_shell = _publicize_package_fields(shell)
2379
2401
  resolved_key = str(public_shell.get("app_key") or "").strip()
@@ -2471,7 +2493,7 @@ class AiBuilderTools(ToolBase):
2471
2493
  if bool(existing.get("created")) and not deferred_add_fields and not update_fields and not remove_fields:
2472
2494
  shell_result = existing.get("shell_result") if isinstance(existing.get("shell_result"), dict) else {}
2473
2495
  layout_sections = list(compiled_item.get("_inline_form_layout_sections") or [])
2474
- if layout_sections:
2496
+ if layout_sections and not _inline_form_layout_was_handled(shell_result):
2475
2497
  shell_result = self._apply_inline_form_layout_after_schema(
2476
2498
  profile=profile,
2477
2499
  schema_result=shell_result,
@@ -2522,10 +2544,11 @@ class AiBuilderTools(ToolBase):
2522
2544
  add_fields=deferred_add_fields,
2523
2545
  update_fields=update_fields,
2524
2546
  remove_fields=remove_fields,
2547
+ inline_form_layout_sections=list(compiled_item.get("_inline_form_layout_sections") or []),
2525
2548
  )
2526
2549
  public_result = _publicize_package_fields(field_result)
2527
2550
  layout_sections = list(compiled_item.get("_inline_form_layout_sections") or [])
2528
- if layout_sections:
2551
+ if layout_sections and not _inline_form_layout_was_handled(public_result):
2529
2552
  public_result = self._apply_inline_form_layout_after_schema(
2530
2553
  profile=profile,
2531
2554
  schema_result=public_result,
@@ -2632,6 +2655,7 @@ class AiBuilderTools(ToolBase):
2632
2655
  inline_form_layout_sections: list[JSONObject] | None = None,
2633
2656
  ) -> JSONObject:
2634
2657
  """执行内部辅助逻辑。"""
2658
+ started_at = time.perf_counter()
2635
2659
  effective_app_name = app_name or app_title
2636
2660
  system_field_failure = _reserved_system_field_name_failure(
2637
2661
  tool_name="app_schema_apply",
@@ -2686,6 +2710,10 @@ class AiBuilderTools(ToolBase):
2686
2710
  parsed_add = [FieldPatch.model_validate(item) for item in plan_args.get("add_fields") or []]
2687
2711
  parsed_update = [FieldUpdatePatch.model_validate(item) for item in plan_args.get("update_fields") or []]
2688
2712
  parsed_remove = [FieldRemovePatch.model_validate(item) for item in plan_args.get("remove_fields") or []]
2713
+ parsed_inline_layout = [
2714
+ LayoutSectionPatch.model_validate(item)
2715
+ for item in (inline_form_layout_sections or [])
2716
+ ]
2689
2717
  except ValidationError as exc:
2690
2718
  return _validation_failure(
2691
2719
  str(exc),
@@ -2721,6 +2749,16 @@ class AiBuilderTools(ToolBase):
2721
2749
  "add_fields": [patch.model_dump(mode="json") for patch in parsed_add],
2722
2750
  "update_fields": [patch.model_dump(mode="json") for patch in parsed_update],
2723
2751
  "remove_fields": [patch.model_dump(mode="json") for patch in parsed_remove],
2752
+ **(
2753
+ {
2754
+ "inline_form_layout_sections": [
2755
+ section.model_dump(mode="json", exclude_none=True)
2756
+ for section in parsed_inline_layout
2757
+ ]
2758
+ }
2759
+ if parsed_inline_layout
2760
+ else {}
2761
+ ),
2724
2762
  }
2725
2763
  result = _safe_tool_call(
2726
2764
  lambda: self._facade.app_schema_apply(
@@ -2736,13 +2774,15 @@ class AiBuilderTools(ToolBase):
2736
2774
  add_fields=parsed_add,
2737
2775
  update_fields=parsed_update,
2738
2776
  remove_fields=parsed_remove,
2777
+ inline_form_layout_sections=parsed_inline_layout,
2739
2778
  ),
2740
2779
  error_code="SCHEMA_APPLY_FAILED",
2741
2780
  normalized_args=normalized_args,
2742
2781
  suggested_next_call={"tool_name": "app_schema_apply", "arguments": {"profile": profile, **normalized_args}},
2743
2782
  )
2744
2783
  public_result = _publicize_package_fields(result)
2745
- if inline_form_layout_sections:
2784
+ if inline_form_layout_sections and not _inline_form_layout_was_handled(public_result):
2785
+ layout_started_at = time.perf_counter()
2746
2786
  public_result = self._apply_inline_form_layout_after_schema(
2747
2787
  profile=profile,
2748
2788
  schema_result=public_result,
@@ -2750,6 +2790,11 @@ class AiBuilderTools(ToolBase):
2750
2790
  publish=publish,
2751
2791
  sections=inline_form_layout_sections,
2752
2792
  )
2793
+ _merge_duration_breakdown(
2794
+ public_result,
2795
+ inline_layout_ms=_elapsed_ms(layout_started_at),
2796
+ )
2797
+ _merge_duration_breakdown(public_result, total_ms=_elapsed_ms(started_at))
2753
2798
  return public_result
2754
2799
 
2755
2800
  def _apply_inline_form_layout_after_schema(