@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 +2 -2
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/skills/qingflow-record-analysis/SKILL.md +1 -1
- package/skills/qingflow-record-analysis/references/analysis-gotchas.md +1 -0
- package/skills/qingflow-record-analysis/references/analysis-patterns.md +4 -3
- package/src/qingflow_mcp/response_trim.py +2 -0
- package/src/qingflow_mcp/server_app_user.py +1 -1
- package/src/qingflow_mcp/tools/record_tools.py +151 -6
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
|
+
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.
|
|
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
package/pyproject.toml
CHANGED
|
@@ -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[]`
|
|
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.
|
|
32
|
-
5.
|
|
33
|
-
6. `record_access.
|
|
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
|
-
"
|
|
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"
|