@qingflow-tech/qingflow-app-builder-mcp 1.0.12 → 1.0.14

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 @qingflow-tech/qingflow-app-builder-mcp@1.0.12
6
+ npm install @qingflow-tech/qingflow-app-builder-mcp@1.0.14
7
7
  ```
8
8
 
9
9
  Run:
10
10
 
11
11
  ```bash
12
- npx -y -p @qingflow-tech/qingflow-app-builder-mcp@1.0.12 qingflow-app-builder-mcp
12
+ npx -y -p @qingflow-tech/qingflow-app-builder-mcp@1.0.14 qingflow-app-builder-mcp
13
13
  ```
14
14
 
15
15
  Environment:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qingflow-tech/qingflow-app-builder-mcp",
3
- "version": "1.0.12",
3
+ "version": "1.0.14",
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 = "1.0.12"
7
+ version = "1.0.14"
8
8
  description = "User-authenticated MCP server for Qingflow"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -1610,6 +1610,7 @@ class AssociatedResourcesApplyRequest(StrictModel):
1610
1610
  if not isinstance(value, dict):
1611
1611
  return value
1612
1612
  payload = dict(value)
1613
+ default_target_app_key = str(payload.get("app_key", payload.get("appKey", "")) or "").strip()
1613
1614
  if "upsertResources" in payload and "upsert_resources" not in payload:
1614
1615
  payload["upsert_resources"] = payload.pop("upsertResources")
1615
1616
  if "patchResources" in payload and "patch_resources" not in payload:
@@ -1622,6 +1623,16 @@ class AssociatedResourcesApplyRequest(StrictModel):
1622
1623
  payload["reorder_associated_item_ids"] = payload.pop("reorderAssociatedItemIds")
1623
1624
  if "viewConfigs" in payload and "view_configs" not in payload:
1624
1625
  payload["view_configs"] = payload.pop("viewConfigs")
1626
+ if default_target_app_key and isinstance(payload.get("upsert_resources"), list):
1627
+ normalized_resources = []
1628
+ for item in payload["upsert_resources"]:
1629
+ if isinstance(item, dict) and not any(
1630
+ str(item.get(key) or "").strip()
1631
+ for key in ("target_app_key", "targetAppKey", "app_key", "appKey")
1632
+ ):
1633
+ item = {**item, "target_app_key": default_target_app_key}
1634
+ normalized_resources.append(item)
1635
+ payload["upsert_resources"] = normalized_resources
1625
1636
  return payload
1626
1637
 
1627
1638
  @model_validator(mode="after")
@@ -17721,8 +17721,23 @@ def _apply_field_mutation(field: dict[str, Any], mutation: Any) -> None:
17721
17721
  field["config"] = deepcopy(payload["code_block_config"])
17722
17722
  question_rebuild_required = True
17723
17723
  if "code_block_binding" in payload:
17724
+ existing_code_block_config = _normalize_code_block_config(field.get("code_block_config") or field.get("config") or {})
17725
+ next_code_block_binding = _normalize_code_block_binding(payload["code_block_binding"])
17724
17726
  field["code_block_binding"] = payload["code_block_binding"]
17725
17727
  field["_explicit_code_block_binding"] = True
17728
+ if (
17729
+ "code_block_config" not in payload
17730
+ and existing_code_block_config is not None
17731
+ and next_code_block_binding is not None
17732
+ ):
17733
+ existing_code = _normalize_code_block_output_assignment(
17734
+ _strip_code_block_generated_input_prelude(str(existing_code_block_config.get("code_content") or ""))
17735
+ )
17736
+ next_code = _normalize_code_block_output_assignment(str(next_code_block_binding.get("code") or ""))
17737
+ if existing_code != next_code:
17738
+ existing_code_block_config["code_content"] = next_code
17739
+ field["code_block_config"] = existing_code_block_config
17740
+ field["config"] = deepcopy(existing_code_block_config)
17726
17741
  question_rebuild_required = True
17727
17742
  if "auto_trigger" in payload:
17728
17743
  field["auto_trigger"] = payload["auto_trigger"]
@@ -18228,6 +18243,15 @@ def _compile_code_block_binding_fields(
18228
18243
  "alias_id": _coerce_positive_int(output_item.get("alias_id")),
18229
18244
  }
18230
18245
  )
18246
+ current_field_refs: set[int] = set()
18247
+ for field in next_fields:
18248
+ if not isinstance(field, dict):
18249
+ continue
18250
+ field_ref = _coerce_positive_int(field.get("que_id"))
18251
+ if field_ref is None:
18252
+ field_ref = _coerce_any_int(field.get("que_temp_id"))
18253
+ if field_ref is not None:
18254
+ current_field_refs.add(field_ref)
18231
18255
  carried_relations: list[dict[str, Any]] = []
18232
18256
  for relation in existing_relations:
18233
18257
  if not isinstance(relation, dict):
@@ -18238,6 +18262,8 @@ def _compile_code_block_binding_fields(
18238
18262
  alias_config = relation.get("aliasConfig") if isinstance(relation.get("aliasConfig"), dict) else {}
18239
18263
  source_ref = _coerce_positive_int(alias_config.get("queId"))
18240
18264
  target_ref = _coerce_positive_int(relation.get("queId"))
18265
+ if source_ref not in current_field_refs or target_ref not in current_field_refs:
18266
+ continue
18241
18267
  if (source_ref is not None and source_ref in affected_source_refs) or (target_ref is not None and target_ref in affected_target_refs):
18242
18268
  continue
18243
18269
  carried_relations.append(deepcopy(relation))
@@ -80,11 +80,33 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
80
80
 
81
81
  list_parser = record_subparsers.add_parser("list", help="列出记录")
82
82
  list_parser.add_argument("--app-key", required=True)
83
- list_parser.add_argument("--column", dest="columns", action="append", type=int, default=[])
84
- list_parser.add_argument("--columns-file")
83
+ list_parser.add_argument(
84
+ "--column",
85
+ dest="columns",
86
+ action="append",
87
+ type=int,
88
+ default=[],
89
+ metavar="FIELD_ID",
90
+ help="只返回这些 field_id;可重复传",
91
+ )
92
+ list_parser.add_argument(
93
+ "--columns-file",
94
+ help="JSON/YAML list;元素为 field_id 整数/整数字符串或 {'field_id': ...}",
95
+ )
85
96
  list_parser.add_argument("--query")
86
- list_parser.add_argument("--query-field", dest="query_fields", action="append", type=int, default=[])
87
- list_parser.add_argument("--query-fields-file")
97
+ list_parser.add_argument(
98
+ "--query-field",
99
+ dest="query_fields",
100
+ action="append",
101
+ type=int,
102
+ default=[],
103
+ metavar="FIELD_ID",
104
+ help="全文搜索范围 field_id;可重复传",
105
+ )
106
+ list_parser.add_argument(
107
+ "--query-fields-file",
108
+ help="JSON/YAML list;元素为 field_id 整数/整数字符串或 {'field_id': ...}",
109
+ )
88
110
  list_parser.add_argument("--where-file")
89
111
  list_parser.add_argument("--order-by-file")
90
112
  list_parser.add_argument("--page", type=int, default=1)
@@ -96,8 +118,19 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
96
118
 
97
119
  access_parser = record_subparsers.add_parser("access", help="访问记录并写入本地 CSV 分片")
98
120
  access_parser.add_argument("--app-key", required=True)
99
- access_parser.add_argument("--column", dest="columns", action="append", type=int, default=[])
100
- access_parser.add_argument("--columns-file")
121
+ access_parser.add_argument(
122
+ "--column",
123
+ dest="columns",
124
+ action="append",
125
+ type=int,
126
+ default=[],
127
+ metavar="FIELD_ID",
128
+ help="导出这些 field_id 到 CSV;可重复传",
129
+ )
130
+ access_parser.add_argument(
131
+ "--columns-file",
132
+ help="JSON/YAML list;元素为 field_id 整数/整数字符串或 {'field_id': ...}",
133
+ )
101
134
  access_parser.add_argument("--where-file")
102
135
  access_parser.add_argument("--order-by-file")
103
136
  access_parser.add_argument("--view-id", required=True)
@@ -106,8 +139,19 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
106
139
  get = record_subparsers.add_parser("get", help="读取单条记录")
107
140
  get.add_argument("--app-key", required=True)
108
141
  get.add_argument("--record-id", required=True)
109
- get.add_argument("--column", dest="columns", action="append", type=int, default=[])
110
- get.add_argument("--columns-file")
142
+ get.add_argument(
143
+ "--column",
144
+ dest="columns",
145
+ action="append",
146
+ type=int,
147
+ default=[],
148
+ metavar="FIELD_ID",
149
+ help="聚焦这些 field_id;可重复传",
150
+ )
151
+ get.add_argument(
152
+ "--columns-file",
153
+ help="JSON/YAML list;元素为 field_id 整数/整数字符串或 {'field_id': ...}",
154
+ )
111
155
  get.add_argument("--view-id")
112
156
  get.set_defaults(handler=_handle_get, format_hint="record_get")
113
157
 
@@ -918,6 +918,12 @@ def _compact_import_column(item: dict[str, Any]) -> dict[str, Any]:
918
918
  compact["options"] = options
919
919
  if bool(item.get("accepts_natural_input")):
920
920
  compact["accepts_natural_input"] = True
921
+ import_value_format = item.get("import_value_format")
922
+ if isinstance(import_value_format, str) and import_value_format:
923
+ compact["import_value_format"] = import_value_format
924
+ format_hint = item.get("format_hint")
925
+ if isinstance(format_hint, str) and format_hint:
926
+ compact["format_hint"] = format_hint
921
927
  if bool(item.get("requires_upload")):
922
928
  compact["requires_upload"] = True
923
929
  target_app_key = item.get("target_app_key")
@@ -269,6 +269,7 @@ If the current MCP capability is unsupported, the workflow is awkward, or the us
269
269
  columns: list[dict | int] | None = None,
270
270
  where: list[dict] | None = None,
271
271
  order_by: list[dict] | None = None,
272
+ record_id: str | int | None = None,
272
273
  record_ids: list[str | int] | None = None,
273
274
  include_workflow_log: bool = False,
274
275
  ) -> dict:
@@ -279,6 +280,7 @@ If the current MCP capability is unsupported, the workflow is awkward, or the us
279
280
  columns=columns or [],
280
281
  where=where or [],
281
282
  order_by=order_by or [],
283
+ record_id=record_id,
282
284
  record_ids=record_ids or [],
283
285
  include_workflow_log=include_workflow_log,
284
286
  )
@@ -310,6 +312,7 @@ If the current MCP capability is unsupported, the workflow is awkward, or the us
310
312
  columns: list[dict | int] | None = None,
311
313
  where: list[dict] | None = None,
312
314
  order_by: list[dict] | None = None,
315
+ record_id: str | int | None = None,
313
316
  record_ids: list[str | int] | None = None,
314
317
  include_workflow_log: bool = False,
315
318
  download_to_path: str | None = None,
@@ -322,6 +325,7 @@ If the current MCP capability is unsupported, the workflow is awkward, or the us
322
325
  columns=columns or [],
323
326
  where=where or [],
324
327
  order_by=order_by or [],
328
+ record_id=record_id,
325
329
  record_ids=record_ids or [],
326
330
  include_workflow_log=include_workflow_log,
327
331
  download_to_path=download_to_path,
@@ -176,17 +176,21 @@ class SessionStore:
176
176
  return memory_session
177
177
  if not session_profile:
178
178
  return None
179
- token = self._get_secret(self._token_key(profile)) if session_profile.persisted else None
180
- if not token:
181
- token = session_profile.token
179
+ token = session_profile.token
180
+ if not token and session_profile.persisted:
181
+ token = self._get_secret(self._token_key(profile))
182
182
  if not token:
183
183
  return None
184
- login_token = self._get_secret(self._login_token_key(profile)) if session_profile.persisted else None
185
- credential = self._get_secret(self._credential_key(profile)) if session_profile.persisted else None
184
+ login_token = session_profile.login_token
185
+ if not login_token and session_profile.persisted:
186
+ login_token = self._get_secret(self._login_token_key(profile))
187
+ credential = session_profile.credential
188
+ if not credential and session_profile.persisted:
189
+ credential = self._get_secret(self._credential_key(profile))
186
190
  backend_session = BackendSession(
187
191
  token=token,
188
- login_token=login_token or session_profile.login_token,
189
- credential=credential or session_profile.credential,
192
+ login_token=login_token,
193
+ credential=credential,
190
194
  profile=profile,
191
195
  base_url=session_profile.base_url,
192
196
  qf_version=session_profile.qf_version,
@@ -104,11 +104,12 @@ class CodeBlockTools(RecordTools):
104
104
 
105
105
  @mcp.tool()
106
106
  def record_code_block_schema_get(
107
+ profile: str = DEFAULT_PROFILE,
107
108
  app_key: str = "",
108
109
  output_profile: str = "normal",
109
110
  ) -> JSONObject:
110
111
  return self.record_code_block_schema_get_public(
111
- profile=DEFAULT_PROFILE,
112
+ profile=profile,
112
113
  app_key=app_key,
113
114
  output_profile=output_profile,
114
115
  )
@@ -65,6 +65,7 @@ class ExportTools(ToolBase):
65
65
  columns: list[JSONObject | int] | None = None,
66
66
  where: list[JSONObject] | None = None,
67
67
  order_by: list[JSONObject] | None = None,
68
+ record_id: str | int | None = None,
68
69
  record_ids: list[str | int] | None = None,
69
70
  include_workflow_log: bool = False,
70
71
  ) -> dict[str, Any]:
@@ -75,6 +76,7 @@ class ExportTools(ToolBase):
75
76
  columns=columns or [],
76
77
  where=where or [],
77
78
  order_by=order_by or [],
79
+ record_id=record_id,
78
80
  record_ids=record_ids or [],
79
81
  include_workflow_log=include_workflow_log,
80
82
  )
@@ -109,6 +111,7 @@ class ExportTools(ToolBase):
109
111
  columns: list[JSONObject | int] | None = None,
110
112
  where: list[JSONObject] | None = None,
111
113
  order_by: list[JSONObject] | None = None,
114
+ record_id: str | int | None = None,
112
115
  record_ids: list[str | int] | None = None,
113
116
  include_workflow_log: bool = False,
114
117
  download_to_path: str | None = None,
@@ -121,6 +124,7 @@ class ExportTools(ToolBase):
121
124
  columns=columns or [],
122
125
  where=where or [],
123
126
  order_by=order_by or [],
127
+ record_id=record_id,
124
128
  record_ids=record_ids or [],
125
129
  include_workflow_log=include_workflow_log,
126
130
  download_to_path=download_to_path,
@@ -137,6 +141,7 @@ class ExportTools(ToolBase):
137
141
  columns: list[JSONObject | int] | None = None,
138
142
  where: list[JSONObject] | None = None,
139
143
  order_by: list[JSONObject] | None = None,
144
+ record_id: str | int | None = None,
140
145
  record_ids: list[str | int] | None = None,
141
146
  include_workflow_log: bool = False,
142
147
  ) -> dict[str, Any]:
@@ -145,7 +150,7 @@ class ExportTools(ToolBase):
145
150
  normalized_columns = _normalize_export_columns(columns or [])
146
151
  normalized_where = self._record_tools._normalize_record_list_where(where or [])
147
152
  normalized_order_by = self._record_tools._normalize_record_list_order_by(order_by or [])
148
- normalized_record_ids = _normalize_export_record_ids(record_ids or [])
153
+ normalized_record_ids = _normalize_export_record_ids(_merge_single_export_record_id(record_id, record_ids or []))
149
154
  if not normalized_app_key:
150
155
  return self._failed_export_result(
151
156
  error_code="EXPORT_START_FAILED",
@@ -467,6 +472,7 @@ class ExportTools(ToolBase):
467
472
  columns: list[JSONObject | int] | None = None,
468
473
  where: list[JSONObject] | None = None,
469
474
  order_by: list[JSONObject] | None = None,
475
+ record_id: str | int | None = None,
470
476
  record_ids: list[str | int] | None = None,
471
477
  include_workflow_log: bool = False,
472
478
  download_to_path: str | None = None,
@@ -496,6 +502,7 @@ class ExportTools(ToolBase):
496
502
  columns=columns or [],
497
503
  where=where or [],
498
504
  order_by=order_by or [],
505
+ record_id=record_id,
499
506
  record_ids=record_ids or [],
500
507
  include_workflow_log=include_workflow_log,
501
508
  )
@@ -1665,6 +1672,12 @@ def _normalize_export_record_ids(record_ids: list[str | int]) -> list[int]:
1665
1672
  return normalized
1666
1673
 
1667
1674
 
1675
+ def _merge_single_export_record_id(record_id: str | int | None, record_ids: list[str | int]) -> list[str | int]:
1676
+ if record_id is None or str(record_id).strip() == "":
1677
+ return list(record_ids)
1678
+ return [record_id, *record_ids]
1679
+
1680
+
1668
1681
  def _effective_total(page: JSONObject, *, page_size: int) -> int:
1669
1682
  rows = page.get("list")
1670
1683
  returned_rows = len(rows) if isinstance(rows, list) else 0
@@ -75,11 +75,12 @@ class ImportTools(ToolBase):
75
75
  """注册当前工具到 MCP 服务。"""
76
76
  @mcp.tool()
77
77
  def record_import_schema_get(
78
+ profile: str = DEFAULT_PROFILE,
78
79
  app_key: str = "",
79
80
  output_profile: str = "normal",
80
81
  ) -> dict[str, Any]:
81
82
  return self.record_import_schema_get(
82
- profile=DEFAULT_PROFILE,
83
+ profile=profile,
83
84
  app_key=app_key,
84
85
  output_profile=output_profile,
85
86
  )
@@ -224,8 +225,15 @@ class ImportTools(ToolBase):
224
225
  }
225
226
  if isinstance(column.get("options"), list) and column.get("options"):
226
227
  payload["options"] = column["options"]
227
- if bool(column.get("requires_lookup")):
228
- payload["accepts_natural_input"] = True
228
+ if column["write_kind"] == "member":
229
+ payload["import_value_format"] = "member_email"
230
+ payload["format_hint"] = "Import files must use member email values."
231
+ elif column["write_kind"] == "relation":
232
+ payload["import_value_format"] = "target_apply_id"
233
+ payload["format_hint"] = "Import files must use the target record apply_id."
234
+ elif column["write_kind"] == "department":
235
+ payload["import_value_format"] = "department_name"
236
+ payload["format_hint"] = "Import files may use a department name within the field candidate scope."
229
237
  if bool(column.get("requires_upload")):
230
238
  payload["requires_upload"] = True
231
239
  if isinstance(column.get("target_app_key"), str):
@@ -660,7 +668,8 @@ class ImportTools(ToolBase):
660
668
  applied_repairs: list[str] = []
661
669
  skipped_repairs: list[str] = []
662
670
  if "normalize_headers" in normalized_repairs:
663
- if _repair_headers(sheet, expected_columns):
671
+ repair_header_columns = _repair_header_columns_from_stored_precheck(stored, expected_columns)
672
+ if _repair_headers(sheet, repair_header_columns):
664
673
  applied_repairs.append("normalize_headers")
665
674
  else:
666
675
  skipped_repairs.append("normalize_headers")
@@ -744,11 +753,17 @@ class ImportTools(ToolBase):
744
753
  return self._failed_start_result(error_code="IMPORT_VERIFICATION_STALE", message="verification_id does not belong to the requested app")
745
754
  if not bool(stored.get("can_import")):
746
755
  return self._failed_start_result(error_code="IMPORT_VERIFICATION_FAILED", message="verification_id is not importable", extra={"accepted": False})
747
- current_path = Path(str(stored.get("verified_file_path") or stored["file_path"]))
756
+ source_local_precheck = stored.get("source_local_precheck")
757
+ source_precheck_passed = isinstance(source_local_precheck, dict) and bool(source_local_precheck.get("can_import"))
758
+ if source_precheck_passed:
759
+ current_path = Path(str(stored.get("source_file_path") or stored["file_path"]))
760
+ expected_sha256 = stored.get("file_sha256")
761
+ else:
762
+ current_path = Path(str(stored.get("verified_file_path") or stored.get("source_file_path") or stored["file_path"]))
763
+ expected_sha256 = stored.get("verified_file_sha256") or stored.get("file_sha256")
748
764
  if not current_path.is_file():
749
- return self._failed_start_result(error_code="IMPORT_VERIFICATION_STALE", message="verified file no longer exists")
765
+ return self._failed_start_result(error_code="IMPORT_VERIFICATION_STALE", message="verified import file no longer exists")
750
766
  current_sha256 = _sha256_file(current_path)
751
- expected_sha256 = stored.get("verified_file_sha256") or stored.get("file_sha256")
752
767
  if current_sha256 != expected_sha256:
753
768
  return self._failed_start_result(
754
769
  error_code="IMPORT_FILE_CHANGED_AFTER_VERIFY",
@@ -1142,6 +1157,9 @@ class ImportTools(ToolBase):
1142
1157
  "can_import": True,
1143
1158
  "extension": extension,
1144
1159
  "error_code": None,
1160
+ "expected_header_titles": list(allowed_header_titles)
1161
+ if allowed_header_titles
1162
+ else [str(item["title"]) for item in expected_columns],
1145
1163
  }
1146
1164
  if extension not in SUPPORTED_IMPORT_EXTENSIONS:
1147
1165
  base_result["issues"].append(_issue("UNSUPPORTED_FILE_FORMAT", "Only .xlsx and .xls files are supported in import v1.", severity="error"))
@@ -2204,6 +2222,20 @@ def _sheet_header_map(sheet) -> dict[str, int]: # type: ignore[no-untyped-def]
2204
2222
  return mapping
2205
2223
 
2206
2224
 
2225
+ def _repair_header_columns_from_stored_precheck(stored: JSONObject, expected_columns: list[JSONObject]) -> list[JSONObject]:
2226
+ for key in ("source_local_precheck", "local_precheck"):
2227
+ precheck = stored.get(key)
2228
+ if not isinstance(precheck, dict):
2229
+ continue
2230
+ titles = precheck.get("expected_header_titles")
2231
+ if not isinstance(titles, list):
2232
+ continue
2233
+ normalized_titles = [title for title in (_normalize_optional_text(item) for item in titles) if title]
2234
+ if normalized_titles:
2235
+ return [{"title": title} for title in normalized_titles]
2236
+ return expected_columns
2237
+
2238
+
2207
2239
  def _repair_headers(sheet, expected_columns: list[JSONObject]) -> bool: # type: ignore[no-untyped-def]
2208
2240
  changed = False
2209
2241
  expected_by_key = {_normalize_header_key(item["title"]): item["title"] for item in expected_columns}
@@ -2222,9 +2254,11 @@ def _repair_headers(sheet, expected_columns: list[JSONObject]) -> bool: # type:
2222
2254
  # Fallback for template-based files where headers were edited into non-canonical
2223
2255
  # values but column order is still intact. Keep any extra trailing system columns.
2224
2256
  for index, column in enumerate(expected_columns, start=1):
2225
- if index > len(header_cells):
2226
- break
2227
2257
  expected_title = str(column["title"]).strip()
2258
+ if index > len(header_cells):
2259
+ sheet.cell(row=1, column=index, value=expected_title)
2260
+ changed = True
2261
+ continue
2228
2262
  current_title = _normalize_optional_text(header_cells[index - 1].value)
2229
2263
  if current_title != expected_title:
2230
2264
  header_cells[index - 1].value = expected_title
@@ -350,11 +350,12 @@ class RecordTools(ToolBase):
350
350
  """注册当前工具到 MCP 服务。"""
351
351
  @mcp.tool()
352
352
  def record_insert_schema_get(
353
+ profile: str = DEFAULT_PROFILE,
353
354
  app_key: str = "",
354
355
  output_profile: str = "normal",
355
356
  ) -> JSONObject:
356
357
  return self.record_insert_schema_get_public(
357
- profile=DEFAULT_PROFILE,
358
+ profile=profile,
358
359
  app_key=app_key,
359
360
  output_profile=output_profile,
360
361
  )
@@ -464,6 +465,7 @@ class RecordTools(ToolBase):
464
465
  )
465
466
  )
466
467
  def record_access(
468
+ profile: str = DEFAULT_PROFILE,
467
469
  app_key: str = "",
468
470
  view_id: str = "",
469
471
  columns: list[JSONObject | int] | None = None,
@@ -471,7 +473,7 @@ class RecordTools(ToolBase):
471
473
  order_by: list[JSONObject] | None = None,
472
474
  ) -> JSONObject:
473
475
  return self.record_access(
474
- profile=DEFAULT_PROFILE,
476
+ profile=profile,
475
477
  app_key=app_key,
476
478
  view_id=view_id,
477
479
  columns=columns or [],
@@ -515,12 +517,13 @@ class RecordTools(ToolBase):
515
517
 
516
518
  @mcp.tool()
517
519
  def record_browse_schema_get(
520
+ profile: str = DEFAULT_PROFILE,
518
521
  app_key: str = "",
519
522
  view_id: str = "",
520
523
  output_profile: str = "normal",
521
524
  ) -> JSONObject:
522
525
  return self.record_browse_schema_get_public(
523
- profile=DEFAULT_PROFILE,
526
+ profile=profile,
524
527
  app_key=app_key,
525
528
  view_id=view_id,
526
529
  output_profile=output_profile,
@@ -528,13 +531,14 @@ class RecordTools(ToolBase):
528
531
 
529
532
  @mcp.tool()
530
533
  def record_update_schema_get(
534
+ profile: str = DEFAULT_PROFILE,
531
535
  app_key: str = "",
532
536
  record_id: str = "",
533
537
  view_id: str | None = None,
534
538
  output_profile: str = "normal",
535
539
  ) -> JSONObject:
536
540
  return self.record_update_schema_get_public(
537
- profile=DEFAULT_PROFILE,
541
+ profile=profile,
538
542
  app_key=app_key,
539
543
  record_id=record_id,
540
544
  view_id=view_id,
@@ -550,13 +554,14 @@ class RecordTools(ToolBase):
550
554
  )
551
555
  )
552
556
  def record_insert(
557
+ profile: str = DEFAULT_PROFILE,
553
558
  app_key: str = "",
554
559
  items: list[JSONObject] | None = None,
555
560
  verify_write: bool = True,
556
561
  output_profile: str = "normal",
557
562
  ) -> JSONObject:
558
563
  return self.record_insert_public(
559
- profile=DEFAULT_PROFILE,
564
+ profile=profile,
560
565
  app_key=app_key,
561
566
  items=items,
562
567
  verify_write=verify_write,
@@ -573,6 +578,7 @@ class RecordTools(ToolBase):
573
578
  )
574
579
  )
575
580
  def record_update(
581
+ profile: str = DEFAULT_PROFILE,
576
582
  app_key: str = "",
577
583
  record_id: str | None = None,
578
584
  fields: JSONObject | None = None,
@@ -583,7 +589,7 @@ class RecordTools(ToolBase):
583
589
  output_profile: str = "normal",
584
590
  ) -> JSONObject:
585
591
  return self.record_update_public(
586
- profile=DEFAULT_PROFILE,
592
+ profile=profile,
587
593
  app_key=app_key,
588
594
  record_id=record_id,
589
595
  fields=fields,
@@ -601,6 +607,7 @@ class RecordTools(ToolBase):
601
607
  )
602
608
  )
603
609
  def record_delete(
610
+ profile: str = DEFAULT_PROFILE,
604
611
  app_key: str = "",
605
612
  record_id: str | None = None,
606
613
  record_ids: list[str] | None = None,
@@ -608,7 +615,7 @@ class RecordTools(ToolBase):
608
615
  output_profile: str = "normal",
609
616
  ) -> JSONObject:
610
617
  return self.record_delete_public(
611
- profile=DEFAULT_PROFILE,
618
+ profile=profile,
612
619
  app_key=app_key,
613
620
  record_id=record_id,
614
621
  record_ids=record_ids or [],
@@ -11896,6 +11903,8 @@ class RecordTools(ToolBase):
11896
11903
  schema: JSONObject = {}
11897
11904
  if isinstance(raw.get("subQuestions"), list):
11898
11905
  schema["formQues"] = [raw["subQuestions"]]
11906
+ elif isinstance(raw.get("subQues"), list):
11907
+ schema["formQues"] = [raw["subQues"]]
11899
11908
  elif isinstance(raw.get("innerQuestions"), list):
11900
11909
  schema["formQues"] = raw["innerQuestions"]
11901
11910
  index = _build_field_index(schema)
@@ -14045,6 +14054,7 @@ class RecordTools(ToolBase):
14045
14054
  or len(count_mismatches) > mismatch_before
14046
14055
  ):
14047
14056
  continue
14057
+ continue
14048
14058
  expected_value = _canonicalize_answer_value_for_compare(answer, field)
14049
14059
  actual_value = _canonicalize_answer_value_for_compare(actual, field)
14050
14060
  if not _canonical_value_is_empty(expected_value) and _canonical_value_is_empty(actual_value):
@@ -18819,12 +18829,14 @@ def _extract_sort_selector(item: JSONObject) -> JSONValue:
18819
18829
  return None
18820
18830
 
18821
18831
 
18822
- def _normalize_public_column_selectors(columns: list[JSONObject | int]) -> list[int]:
18832
+ def _normalize_public_column_selectors(columns: list[JSONObject | int | str]) -> list[int]:
18823
18833
  normalized: list[int] = []
18824
18834
  for item in columns:
18825
18835
  field_id: int | None = None
18826
18836
  if isinstance(item, int):
18827
18837
  field_id = item
18838
+ elif isinstance(item, str):
18839
+ field_id = _coerce_count(item)
18828
18840
  elif isinstance(item, dict):
18829
18841
  _ensure_allowed_record_list_keys(
18830
18842
  item,
@@ -18836,19 +18848,21 @@ def _normalize_public_column_selectors(columns: list[JSONObject | int]) -> list[
18836
18848
  if field_id is None or field_id < 0:
18837
18849
  raise_tool_error(
18838
18850
  QingflowApiError.config_error(
18839
- "columns must be a list of field_id integers or {field_id} objects"
18851
+ "columns must be a list of field_id integers, integer strings, or {field_id} objects"
18840
18852
  )
18841
18853
  )
18842
18854
  normalized.append(field_id)
18843
18855
  return normalized
18844
18856
 
18845
18857
 
18846
- def _normalize_public_query_field_selectors(query_fields: list[JSONObject | int]) -> list[int]:
18858
+ def _normalize_public_query_field_selectors(query_fields: list[JSONObject | int | str]) -> list[int]:
18847
18859
  normalized: list[int] = []
18848
18860
  for item in query_fields:
18849
18861
  field_id: int | None = None
18850
18862
  if isinstance(item, int):
18851
18863
  field_id = item
18864
+ elif isinstance(item, str):
18865
+ field_id = _coerce_count(item)
18852
18866
  elif isinstance(item, dict):
18853
18867
  _ensure_allowed_record_list_keys(
18854
18868
  item,
@@ -18860,7 +18874,7 @@ def _normalize_public_query_field_selectors(query_fields: list[JSONObject | int]
18860
18874
  if field_id is None or field_id < 0:
18861
18875
  raise_tool_error(
18862
18876
  QingflowApiError.config_error(
18863
- "query_fields must be a list of field_id integers or {field_id} objects"
18877
+ "query_fields must be a list of field_id integers, integer strings, or {field_id} objects"
18864
18878
  )
18865
18879
  )
18866
18880
  normalized.append(field_id)