@josephyan/qingflow-app-user-mcp 1.0.6 → 1.0.7

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.0.6
6
+ npm install @josephyan/qingflow-app-user-mcp@1.0.7
7
7
  ```
8
8
 
9
9
  Run:
10
10
 
11
11
  ```bash
12
- npx -y -p @josephyan/qingflow-app-user-mcp@1.0.6 qingflow-app-user-mcp
12
+ npx -y -p @josephyan/qingflow-app-user-mcp@1.0.7 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.0.6",
3
+ "version": "1.0.7",
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.0.6"
7
+ version = "1.0.7"
8
8
  description = "User-authenticated MCP server for Qingflow"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -73,7 +73,7 @@ Use `record_access` to fetch detail rows into local CSV shards. It does not anal
73
73
  }
74
74
  ```
75
75
 
76
- Then run Python against every `files[].local_path`. CSV columns are stable: `record_id`, then `field_<field_id>`. Use `fields[]` metadata to map titles and types.
76
+ Then run Python against every `files[].local_path`. CSV columns are stable: `record_id`, then `field_<field_id>`. Use `fields[]` plus `metadata_files.schema` / `metadata_files.readme` in the same output directory to map titles and types, especially when reusing the CSV later.
77
77
 
78
78
  - Never ask for `page`, `page_size`, `limit`, or `max_rows`; `record_access` owns paging internally and follows the backend's native paging capability.
79
79
  - If multiple CSV files are returned, read them all.
@@ -94,6 +94,7 @@ Examples of the right recovery question:
94
94
  - Do not invent `page_size`
95
95
  - Do not invent `requested_pages`
96
96
  - Do not invent `scan_max_pages`
97
+ - Do not rename CSV columns before analysis; use `metadata_files.schema` / `metadata_files.readme` to interpret `field_<id>` columns.
97
98
  - Do not invent `auto_expand_pages`
98
99
  - Do not invent `max_rows`
99
100
 
@@ -28,9 +28,10 @@ Result reading order:
28
28
  1. `record_access.complete`
29
29
  2. `record_access.safe_for_final_conclusion`
30
30
  3. `record_access.files[].local_path`
31
- 4. Python outputs
32
- 5. `record_access.fields`
33
- 6. `record_access.warnings`
31
+ 4. `record_access.metadata_files.schema`
32
+ 5. Python outputs
33
+ 6. `record_access.fields`
34
+ 7. `record_access.warnings`
34
35
 
35
36
  Treat `record_browse_schema_get` as the browse-schema source of truth. Missing fields are permission boundaries, not invitations to guess hidden ids.
36
37
 
@@ -455,11 +455,13 @@ def _trim_record_access(payload: JSONObject) -> None:
455
455
  "app_key",
456
456
  "view_id",
457
457
  "format",
458
+ "local_dir",
458
459
  "row_count",
459
460
  "complete",
460
461
  "truncated",
461
462
  "safe_for_final_conclusion",
462
463
  "files",
464
+ "metadata_files",
463
465
  "fields",
464
466
  "warnings",
465
467
  "verification",
@@ -74,7 +74,7 @@ Inside `optional_fields`, any field with `may_become_required=true` is still wri
74
74
 
75
75
  Prefer `view_id` entries from `accessible_views` where `analysis_supported=true`.
76
76
 
77
- Use `record_access` to write local CSV shard files, then use Python to compute counts, rankings, ratios, trends, and final conclusions. `record_access` does not return bulk `items`; read `files[].local_path`.
77
+ Use `record_access` to write local CSV shard files, then use Python to compute counts, rankings, ratios, trends, and final conclusions. `record_access` does not return bulk `items`; read `files[].local_path`. Use `fields[]` and the same-directory `metadata_files.schema` / `metadata_files.readme` to map stable `field_<id>` CSV columns back to titles and types.
78
78
  Use `chart_get` only when the user provides a report URL / chart_id or explicitly asks to read an existing report. Do not use QingBI as the default analysis route.
79
79
 
80
80
  Use this data-access DSL shape:
@@ -1932,6 +1932,7 @@ class RecordTools(ToolBase):
1932
1932
  current_page = 1
1933
1933
  has_more = False
1934
1934
  reported_total: int | None = None
1935
+ created_at = datetime.now(UTC).isoformat()
1935
1936
  try:
1936
1937
  while True:
1937
1938
  if used_list_type is None:
@@ -2049,6 +2050,25 @@ class RecordTools(ToolBase):
2049
2050
  safe_for_final_conclusion = complete and not any(
2050
2051
  warning.get("code") == "CUSTOM_VIEW_FILTER_UNVERIFIED" for warning in warnings
2051
2052
  )
2053
+ fields_payload = [_record_access_field_payload(field) for field in selected_fields]
2054
+ verification_payload: JSONObject = {
2055
+ **_view_filter_verification_payload(view_route),
2056
+ "reported_total": reported_total,
2057
+ "list_type_used": used_list_type,
2058
+ }
2059
+ metadata_files = _write_record_access_metadata_files(
2060
+ run_dir=run_dir,
2061
+ created_at=created_at,
2062
+ app_key=app_key,
2063
+ view_route=view_route,
2064
+ row_count=row_count,
2065
+ complete=complete,
2066
+ safe_for_final_conclusion=safe_for_final_conclusion,
2067
+ files=files,
2068
+ fields=fields_payload,
2069
+ warnings=warnings,
2070
+ verification=verification_payload,
2071
+ )
2052
2072
  return {
2053
2073
  "profile": profile,
2054
2074
  "ws_id": session_profile.selected_ws_id,
@@ -2057,18 +2077,16 @@ class RecordTools(ToolBase):
2057
2077
  "app_key": app_key,
2058
2078
  "view_id": view_route.view_id,
2059
2079
  "format": "csv",
2080
+ "local_dir": str(run_dir),
2060
2081
  "row_count": row_count,
2061
2082
  "complete": complete,
2062
2083
  "truncated": not complete,
2063
2084
  "safe_for_final_conclusion": safe_for_final_conclusion,
2064
2085
  "files": files,
2065
- "fields": [_record_access_field_payload(field) for field in selected_fields],
2086
+ "metadata_files": metadata_files,
2087
+ "fields": fields_payload,
2066
2088
  "warnings": warnings,
2067
- "verification": {
2068
- **_view_filter_verification_payload(view_route),
2069
- "reported_total": reported_total,
2070
- "list_type_used": used_list_type,
2071
- },
2089
+ "verification": verification_payload,
2072
2090
  "request_route": self._request_route_payload(context),
2073
2091
  }
2074
2092
 
@@ -11503,6 +11521,133 @@ def _record_access_field_payload(field: FormField) -> JSONObject:
11503
11521
  }
11504
11522
 
11505
11523
 
11524
+ def _write_record_access_metadata_files(
11525
+ *,
11526
+ run_dir: Path,
11527
+ created_at: str,
11528
+ app_key: str,
11529
+ view_route: AccessibleViewRoute,
11530
+ row_count: int,
11531
+ complete: bool,
11532
+ safe_for_final_conclusion: bool,
11533
+ files: list[JSONObject],
11534
+ fields: list[JSONObject],
11535
+ warnings: list[JSONObject],
11536
+ verification: JSONObject,
11537
+ ) -> JSONObject:
11538
+ schema_path = run_dir / "schema.json"
11539
+ readme_path = run_dir / "README.md"
11540
+ schema_payload: JSONObject = {
11541
+ "created_at": created_at,
11542
+ "app_key": app_key,
11543
+ "view_id": view_route.view_id,
11544
+ "view_name": view_route.name,
11545
+ "view_type": view_route.view_type,
11546
+ "format": "csv",
11547
+ "row_count": row_count,
11548
+ "complete": complete,
11549
+ "truncated": not complete,
11550
+ "safe_for_final_conclusion": safe_for_final_conclusion,
11551
+ "files": files,
11552
+ "fields": fields,
11553
+ "warnings": warnings,
11554
+ "verification": verification,
11555
+ "csv_columns": ["record_id"] + [str(field["column_name"]) for field in fields if field.get("column_name")],
11556
+ }
11557
+ schema_path.write_text(json.dumps(schema_payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
11558
+ readme_path.write_text(
11559
+ _record_access_readme(
11560
+ created_at=created_at,
11561
+ app_key=app_key,
11562
+ view_route=view_route,
11563
+ row_count=row_count,
11564
+ complete=complete,
11565
+ safe_for_final_conclusion=safe_for_final_conclusion,
11566
+ files=files,
11567
+ fields=fields,
11568
+ warnings=warnings,
11569
+ ),
11570
+ encoding="utf-8",
11571
+ )
11572
+ return {
11573
+ "schema": str(schema_path),
11574
+ "readme": str(readme_path),
11575
+ }
11576
+
11577
+
11578
+ def _record_access_readme(
11579
+ *,
11580
+ created_at: str,
11581
+ app_key: str,
11582
+ view_route: AccessibleViewRoute,
11583
+ row_count: int,
11584
+ complete: bool,
11585
+ safe_for_final_conclusion: bool,
11586
+ files: list[JSONObject],
11587
+ fields: list[JSONObject],
11588
+ warnings: list[JSONObject],
11589
+ ) -> str:
11590
+ lines = [
11591
+ "# Qingflow Record Access",
11592
+ "",
11593
+ f"- Created at: {created_at}",
11594
+ f"- App key: `{app_key}`",
11595
+ f"- View: {view_route.name} (`{view_route.view_id}`)",
11596
+ f"- Format: CSV",
11597
+ f"- Rows: {row_count}",
11598
+ f"- Complete: {str(complete).lower()}",
11599
+ f"- Safe for final conclusion: {str(safe_for_final_conclusion).lower()}",
11600
+ "",
11601
+ "## Data Files",
11602
+ "",
11603
+ ]
11604
+ if files:
11605
+ for file_info in files:
11606
+ local_path = _normalize_optional_text(file_info.get("local_path")) or ""
11607
+ part = file_info.get("part")
11608
+ file_rows = file_info.get("row_count")
11609
+ lines.append(f"- `{Path(local_path).name}`: part {part}, {file_rows} rows")
11610
+ else:
11611
+ lines.append("- No CSV shard files were written because the query returned 0 rows.")
11612
+ lines.extend(
11613
+ [
11614
+ "",
11615
+ "## Columns",
11616
+ "",
11617
+ "| CSV column | Field ID | Title | Type |",
11618
+ "|---|---:|---|---|",
11619
+ "| `record_id` | - | Record ID | record_id |",
11620
+ ]
11621
+ )
11622
+ for field in fields:
11623
+ column_name = _markdown_escape(str(field.get("column_name", "")))
11624
+ field_id = field.get("field_id", "")
11625
+ title = _markdown_escape(str(field.get("title", "")))
11626
+ field_type = _markdown_escape(str(field.get("type", "")))
11627
+ lines.append(f"| `{column_name}` | {field_id} | {title} | {field_type} |")
11628
+ if warnings:
11629
+ lines.extend(["", "## Warnings", ""])
11630
+ for warning in warnings:
11631
+ code = _normalize_optional_text(warning.get("code")) or "WARNING"
11632
+ message = _normalize_optional_text(warning.get("message")) or ""
11633
+ lines.append(f"- `{code}`: {message}")
11634
+ lines.extend(
11635
+ [
11636
+ "",
11637
+ "## Usage Notes",
11638
+ "",
11639
+ "- Use `records-*.csv` for Python or pandas processing; column names are stable and field-id based.",
11640
+ "- Use `schema.json` or this README to map `field_<field_id>` columns back to Qingflow field titles and types.",
11641
+ "",
11642
+ ]
11643
+ )
11644
+ return "\n".join(lines)
11645
+
11646
+
11647
+ def _markdown_escape(value: str) -> str:
11648
+ return value.replace("\\", "\\\\").replace("|", "\\|").replace("\n", " ")
11649
+
11650
+
11506
11651
  def _record_access_field_type(field: FormField) -> str:
11507
11652
  if field.que_type in DATE_QUE_TYPES:
11508
11653
  return "datetime"