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

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.42
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.42 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.42",
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.0b42"
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.0b42"
@@ -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
@@ -629,6 +632,7 @@ class ImportTools(ToolBase):
629
632
  path: Path,
630
633
  app_key: str,
631
634
  expected_columns: list[JSONObject],
635
+ allowed_header_titles: list[str] | None,
632
636
  schema_fingerprint: str,
633
637
  ) -> dict[str, Any]:
634
638
  extension = path.suffix.lower()
@@ -681,7 +685,11 @@ class ImportTools(ToolBase):
681
685
  return base_result
682
686
  sheet = workbook[workbook.sheetnames[0]]
683
687
  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)
688
+ header_analysis = _analyze_headers(
689
+ header_row,
690
+ expected_columns,
691
+ allowed_titles=allowed_header_titles,
692
+ )
685
693
  base_result["issues"].extend(header_analysis["issues"])
686
694
  base_result["repair_suggestions"].extend(header_analysis["repair_suggestions"])
687
695
  trailing_blank_rows = _count_trailing_blank_rows(sheet)
@@ -709,6 +717,31 @@ class ImportTools(ToolBase):
709
717
  base_result["error_code"] = "IMPORT_VERIFICATION_FAILED"
710
718
  return base_result
711
719
 
720
+ def _load_template_header_titles(self, context, app_key: str) -> tuple[list[str] | None, list[JSONObject]]: # type: ignore[no-untyped-def]
721
+ warnings: list[JSONObject] = []
722
+ try:
723
+ payload = self.backend.request("GET", context, f"/app/{app_key}/apply/excelTemplate")
724
+ template_url = _pick_template_url(payload)
725
+ if not template_url:
726
+ return None, warnings
727
+ content = self.backend.download_binary(template_url)
728
+ workbook = load_workbook(BytesIO(content), read_only=True, data_only=False)
729
+ if not workbook.sheetnames:
730
+ return None, warnings
731
+ sheet = workbook[workbook.sheetnames[0]]
732
+ header_row = [cell.value for cell in next(sheet.iter_rows(min_row=1, max_row=1), [])]
733
+ titles = [_normalize_optional_text(value) for value in header_row]
734
+ normalized_titles = [title for title in titles if title]
735
+ return normalized_titles or None, warnings
736
+ except Exception:
737
+ warnings.append(
738
+ {
739
+ "code": "IMPORT_TEMPLATE_HEADER_UNAVAILABLE",
740
+ "message": "Official template headers could not be loaded during local precheck; falling back to applicant writable columns only.",
741
+ }
742
+ )
743
+ return None, warnings
744
+
712
745
  def _failed_template_result(
713
746
  self,
714
747
  *,
@@ -919,8 +952,18 @@ def _issue(code: str, message: str, *, severity: str, repairable: bool = False,
919
952
  return payload
920
953
 
921
954
 
922
- def _analyze_headers(header_row: list[Any], expected_columns: list[JSONObject]) -> dict[str, Any]:
955
+ def _analyze_headers(
956
+ header_row: list[Any],
957
+ expected_columns: list[JSONObject],
958
+ *,
959
+ allowed_titles: list[str] | None = None,
960
+ ) -> dict[str, Any]:
923
961
  expected_by_key = {_normalize_header_key(item["title"]): item for item in expected_columns}
962
+ allowed_by_key = (
963
+ {_normalize_header_key(title): title for title in allowed_titles if _normalize_optional_text(title)}
964
+ if allowed_titles
965
+ else {key: item["title"] for key, item in expected_by_key.items()}
966
+ )
924
967
  seen: dict[str, int] = {}
925
968
  actual_headers: list[str] = []
926
969
  for item in header_row:
@@ -932,8 +975,8 @@ def _analyze_headers(header_row: list[Any], expected_columns: list[JSONObject])
932
975
  key = _normalize_header_key(text)
933
976
  seen[key] = seen.get(key, 0) + 1
934
977
  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]
978
+ missing = [title for key, title in allowed_by_key.items() if key not in actual_keys]
979
+ extra = [text for text in actual_headers if text and _normalize_header_key(text) not in allowed_by_key]
937
980
  duplicates = [text for text in actual_headers if text and seen.get(_normalize_header_key(text), 0) > 1]
938
981
  issues: list[JSONObject] = []
939
982
  repair_suggestions: list[str] = []
@@ -947,9 +990,9 @@ def _analyze_headers(header_row: list[Any], expected_columns: list[JSONObject])
947
990
  for text in actual_headers:
948
991
  if not text:
949
992
  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"]))
993
+ canonical = allowed_by_key.get(_normalize_header_key(text))
994
+ if canonical and canonical != text:
995
+ normalized_changes.append((text, canonical))
953
996
  if normalized_changes:
954
997
  repair_suggestions.append("normalize_headers")
955
998
  return {"issues": issues, "repair_suggestions": repair_suggestions}