@josephyan/qingflow-app-user-mcp 0.2.0-beta.41 → 0.2.0-beta.43

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.41
6
+ npm install @josephyan/qingflow-app-user-mcp@0.2.0-beta.43
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.41 qingflow-app-user-mcp
12
+ npx -y -p @josephyan/qingflow-app-user-mcp@0.2.0-beta.43 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.41",
3
+ "version": "0.2.0-beta.43",
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.0b41"
7
+ version = "0.2.0b43"
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.0b41"
5
+ __version__ = "0.2.0b43"
@@ -4,6 +4,7 @@ import hashlib
4
4
  import json
5
5
  import mimetypes
6
6
  import shutil
7
+ from io import BytesIO
7
8
  from copy import deepcopy
8
9
  from datetime import datetime, timedelta, timezone
9
10
  from pathlib import Path
@@ -197,13 +198,15 @@ class ImportTools(ToolBase):
197
198
 
198
199
  def runner(session_profile, context):
199
200
  expected_columns, schema_fingerprint = self._expected_import_columns(profile, context, app_key)
201
+ template_header_titles, header_warnings = self._load_template_header_titles(context, app_key)
200
202
  local_check = self._local_verify(
201
203
  path=path,
202
204
  app_key=app_key,
203
205
  expected_columns=expected_columns,
206
+ allowed_header_titles=template_header_titles,
204
207
  schema_fingerprint=schema_fingerprint,
205
208
  )
206
- warnings = deepcopy(local_check["warnings"])
209
+ warnings = deepcopy(local_check["warnings"]) + header_warnings
207
210
  issues = deepcopy(local_check["issues"])
208
211
  can_import = bool(local_check["can_import"])
209
212
  backend_verification = None
@@ -225,7 +228,8 @@ class ImportTools(ToolBase):
225
228
  backend_verification = payload
226
229
  else:
227
230
  backend_verification = {}
228
- if not bool(backend_verification.get("beingValidated", True)):
231
+ being_validated = backend_verification.get("beingValidated", True)
232
+ if being_validated is False:
229
233
  can_import = False
230
234
  issues.append(
231
235
  _issue(
@@ -285,7 +289,8 @@ class ImportTools(ToolBase):
285
289
  "warnings": warnings,
286
290
  "verification": {
287
291
  "local_precheck_passed": bool(local_check["local_precheck_passed"]),
288
- "backend_verification_passed": can_import and isinstance(backend_verification, dict),
292
+ "backend_verification_passed": isinstance(backend_verification, dict)
293
+ and backend_verification.get("beingValidated", True) is not False,
289
294
  "schema_fingerprint": schema_fingerprint,
290
295
  "file_sha256": local_check["file_sha256"],
291
296
  "file_format": local_check["extension"],
@@ -629,6 +634,7 @@ class ImportTools(ToolBase):
629
634
  path: Path,
630
635
  app_key: str,
631
636
  expected_columns: list[JSONObject],
637
+ allowed_header_titles: list[str] | None,
632
638
  schema_fingerprint: str,
633
639
  ) -> dict[str, Any]:
634
640
  extension = path.suffix.lower()
@@ -681,7 +687,11 @@ class ImportTools(ToolBase):
681
687
  return base_result
682
688
  sheet = workbook[workbook.sheetnames[0]]
683
689
  header_row = [cell.value for cell in next(sheet.iter_rows(min_row=1, max_row=1), [])]
684
- header_analysis = _analyze_headers(header_row, expected_columns)
690
+ header_analysis = _analyze_headers(
691
+ header_row,
692
+ expected_columns,
693
+ allowed_titles=allowed_header_titles,
694
+ )
685
695
  base_result["issues"].extend(header_analysis["issues"])
686
696
  base_result["repair_suggestions"].extend(header_analysis["repair_suggestions"])
687
697
  trailing_blank_rows = _count_trailing_blank_rows(sheet)
@@ -709,6 +719,31 @@ class ImportTools(ToolBase):
709
719
  base_result["error_code"] = "IMPORT_VERIFICATION_FAILED"
710
720
  return base_result
711
721
 
722
+ def _load_template_header_titles(self, context, app_key: str) -> tuple[list[str] | None, list[JSONObject]]: # type: ignore[no-untyped-def]
723
+ warnings: list[JSONObject] = []
724
+ try:
725
+ payload = self.backend.request("GET", context, f"/app/{app_key}/apply/excelTemplate")
726
+ template_url = _pick_template_url(payload)
727
+ if not template_url:
728
+ return None, warnings
729
+ content = self.backend.download_binary(template_url)
730
+ workbook = load_workbook(BytesIO(content), read_only=True, data_only=False)
731
+ if not workbook.sheetnames:
732
+ return None, warnings
733
+ sheet = workbook[workbook.sheetnames[0]]
734
+ header_row = [cell.value for cell in next(sheet.iter_rows(min_row=1, max_row=1), [])]
735
+ titles = [_normalize_optional_text(value) for value in header_row]
736
+ normalized_titles = [title for title in titles if title]
737
+ return normalized_titles or None, warnings
738
+ except Exception:
739
+ warnings.append(
740
+ {
741
+ "code": "IMPORT_TEMPLATE_HEADER_UNAVAILABLE",
742
+ "message": "Official template headers could not be loaded during local precheck; falling back to applicant writable columns only.",
743
+ }
744
+ )
745
+ return None, warnings
746
+
712
747
  def _failed_template_result(
713
748
  self,
714
749
  *,
@@ -919,8 +954,18 @@ def _issue(code: str, message: str, *, severity: str, repairable: bool = False,
919
954
  return payload
920
955
 
921
956
 
922
- def _analyze_headers(header_row: list[Any], expected_columns: list[JSONObject]) -> dict[str, Any]:
957
+ def _analyze_headers(
958
+ header_row: list[Any],
959
+ expected_columns: list[JSONObject],
960
+ *,
961
+ allowed_titles: list[str] | None = None,
962
+ ) -> dict[str, Any]:
923
963
  expected_by_key = {_normalize_header_key(item["title"]): item for item in expected_columns}
964
+ allowed_by_key = (
965
+ {_normalize_header_key(title): title for title in allowed_titles if _normalize_optional_text(title)}
966
+ if allowed_titles
967
+ else {key: item["title"] for key, item in expected_by_key.items()}
968
+ )
924
969
  seen: dict[str, int] = {}
925
970
  actual_headers: list[str] = []
926
971
  for item in header_row:
@@ -932,8 +977,8 @@ def _analyze_headers(header_row: list[Any], expected_columns: list[JSONObject])
932
977
  key = _normalize_header_key(text)
933
978
  seen[key] = seen.get(key, 0) + 1
934
979
  actual_keys = {key for key, count in seen.items() if key and count > 0}
935
- missing = [item["title"] for key, item in expected_by_key.items() if key not in actual_keys]
936
- extra = [text for text in actual_headers if text and _normalize_header_key(text) not in expected_by_key]
980
+ missing = [title for key, title in allowed_by_key.items() if key not in actual_keys]
981
+ extra = [text for text in actual_headers if text and _normalize_header_key(text) not in allowed_by_key]
937
982
  duplicates = [text for text in actual_headers if text and seen.get(_normalize_header_key(text), 0) > 1]
938
983
  issues: list[JSONObject] = []
939
984
  repair_suggestions: list[str] = []
@@ -947,9 +992,9 @@ def _analyze_headers(header_row: list[Any], expected_columns: list[JSONObject])
947
992
  for text in actual_headers:
948
993
  if not text:
949
994
  continue
950
- matched = expected_by_key.get(_normalize_header_key(text))
951
- if matched and matched["title"] != text:
952
- normalized_changes.append((text, matched["title"]))
995
+ canonical = allowed_by_key.get(_normalize_header_key(text))
996
+ if canonical and canonical != text:
997
+ normalized_changes.append((text, canonical))
953
998
  if normalized_changes:
954
999
  repair_suggestions.append("normalize_headers")
955
1000
  return {"issues": issues, "repair_suggestions": repair_suggestions}