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

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.5
6
+ npm install @josephyan/qingflow-app-user-mcp@1.0.6
7
7
  ```
8
8
 
9
9
  Run:
10
10
 
11
11
  ```bash
12
- npx -y -p @josephyan/qingflow-app-user-mcp@1.0.5 qingflow-app-user-mcp
12
+ npx -y -p @josephyan/qingflow-app-user-mcp@1.0.6 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.5",
3
+ "version": "1.0.6",
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.5"
7
+ version = "1.0.6"
8
8
  description = "User-authenticated MCP server for Qingflow"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -75,7 +75,7 @@ Use `record_access` to fetch detail rows into local CSV shards. It does not anal
75
75
 
76
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.
77
77
 
78
- - Never ask for `page`, `page_size`, `limit`, or `max_rows`; `record_access` owns paging internally.
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.
80
80
  - If `complete=false` or `safe_for_final_conclusion=false`, downgrade the answer and disclose the limitation.
81
81
  - `record_export_direct` is only for explicit export/download/Excel requests, not default analysis.
@@ -89,7 +89,7 @@ Examples of the right recovery question:
89
89
 
90
90
  ## Do not try to control paging manually
91
91
 
92
- `record_access` and `record_analyze` hide paging and scan budget on purpose.
92
+ `record_access` hides paging and follows the backend's native paging capability. `record_analyze` hides paging and scan budget on purpose.
93
93
 
94
94
  - Do not invent `page_size`
95
95
  - Do not invent `requested_pages`
@@ -32,9 +32,8 @@ from .directory_tools import _directory_has_more, _directory_items
32
32
 
33
33
  DEFAULT_QUERY_PAGE_SIZE = 50
34
34
  DEFAULT_LIST_PAGE_SIZE = 200
35
+ BACKEND_RECORD_ACCESS_PAGE_SIZE = 1000
35
36
  DEFAULT_RECORD_ACCESS_SHARD_ROWS = 5000
36
- DEFAULT_RECORD_ACCESS_HARD_ROWS = 50000
37
- DEFAULT_RECORD_ACCESS_TIMEOUT_SECONDS = 60
38
37
  DEFAULT_ANALYSIS_PAGE_SIZE = 1000
39
38
  DEFAULT_SCAN_MAX_PAGES = 10
40
39
  DEFAULT_ANALYSIS_SCAN_MAX_PAGES = 100
@@ -1870,7 +1869,6 @@ class RecordTools(ToolBase):
1870
1869
  primary_search_que_ids = primary_search_que_ids or None
1871
1870
  match_rules = self._resolve_match_rules(context, filters, index)
1872
1871
  sort_rules = self._resolve_sorts(sorts, index)
1873
- dept_member_cache: dict[int, set[int]] = {}
1874
1872
  used_list_type: int | None = None
1875
1873
  fallback_list_types = (
1876
1874
  [view_route.list_type if view_route.list_type is not None else DEFAULT_RECORD_LIST_TYPE]
@@ -1934,17 +1932,8 @@ class RecordTools(ToolBase):
1934
1932
  current_page = 1
1935
1933
  has_more = False
1936
1934
  reported_total: int | None = None
1937
- limit_reached = False
1938
- timeout_reached = False
1939
- started_at = time.perf_counter()
1940
1935
  try:
1941
1936
  while True:
1942
- if row_count >= DEFAULT_RECORD_ACCESS_HARD_ROWS:
1943
- limit_reached = True
1944
- break
1945
- if time.perf_counter() - started_at >= DEFAULT_RECORD_ACCESS_TIMEOUT_SECONDS:
1946
- timeout_reached = True
1947
- break
1948
1937
  if used_list_type is None:
1949
1938
  last_error: QingflowApiError | None = None
1950
1939
  page: JSONObject | None = None
@@ -1955,7 +1944,7 @@ class RecordTools(ToolBase):
1955
1944
  app_key=app_key,
1956
1945
  view_selection=view_selection,
1957
1946
  page_num=current_page,
1958
- page_size=DEFAULT_LIST_PAGE_SIZE,
1947
+ page_size=BACKEND_RECORD_ACCESS_PAGE_SIZE,
1959
1948
  query_key=None,
1960
1949
  match_rules=match_rules,
1961
1950
  sorts=sort_rules,
@@ -1982,7 +1971,7 @@ class RecordTools(ToolBase):
1982
1971
  app_key=app_key,
1983
1972
  view_selection=view_selection,
1984
1973
  page_num=current_page,
1985
- page_size=DEFAULT_LIST_PAGE_SIZE,
1974
+ page_size=BACKEND_RECORD_ACCESS_PAGE_SIZE,
1986
1975
  query_key=None,
1987
1976
  match_rules=match_rules,
1988
1977
  sorts=sort_rules,
@@ -1992,25 +1981,15 @@ class RecordTools(ToolBase):
1992
1981
  page_rows = page.get("list")
1993
1982
  items = page_rows if isinstance(page_rows, list) else []
1994
1983
  if reported_total is None:
1995
- reported_total = _effective_total(page, DEFAULT_LIST_PAGE_SIZE)
1996
- has_more = _page_has_more(page, current_page, DEFAULT_LIST_PAGE_SIZE, len(items))
1984
+ reported_total = _effective_total(page, BACKEND_RECORD_ACCESS_PAGE_SIZE)
1985
+ has_more = _page_has_more(page, current_page, BACKEND_RECORD_ACCESS_PAGE_SIZE, len(items))
1997
1986
  page_apply_order: list[int] = []
1998
1987
  page_answer_map: dict[int, list[JSONValue]] = {}
1999
1988
  for item in items:
2000
- if row_count + len(page_apply_order) >= DEFAULT_RECORD_ACCESS_HARD_ROWS:
2001
- limit_reached = True
2002
- break
2003
1989
  if not isinstance(item, dict):
2004
1990
  continue
2005
1991
  answers = item.get("answers")
2006
1992
  answer_list = answers if isinstance(answers, list) else []
2007
- if not self._matches_view_selection(
2008
- context,
2009
- answer_list,
2010
- view_selection=view_selection,
2011
- dept_member_cache=dept_member_cache,
2012
- ):
2013
- continue
2014
1993
  apply_id = _coerce_count(item.get("applyId")) or _coerce_count(item.get("id"))
2015
1994
  if apply_id is None:
2016
1995
  continue
@@ -2023,7 +2002,7 @@ class RecordTools(ToolBase):
2023
2002
  app_key=app_key,
2024
2003
  view_selection=view_selection,
2025
2004
  page_num=current_page,
2026
- page_size=DEFAULT_LIST_PAGE_SIZE,
2005
+ page_size=BACKEND_RECORD_ACCESS_PAGE_SIZE,
2027
2006
  query_key=None,
2028
2007
  match_rules=match_rules,
2029
2008
  sorts=sort_rules,
@@ -2045,11 +2024,8 @@ class RecordTools(ToolBase):
2045
2024
  cast(list[JSONValue], extra_answer_list),
2046
2025
  )
2047
2026
  for apply_id in page_apply_order:
2048
- if row_count >= DEFAULT_RECORD_ACCESS_HARD_ROWS:
2049
- limit_reached = True
2050
- break
2051
2027
  write_record(apply_id, page_answer_map.get(apply_id, []))
2052
- if limit_reached or not has_more:
2028
+ if not has_more:
2053
2029
  break
2054
2030
  current_page += 1
2055
2031
  finally:
@@ -2069,21 +2045,7 @@ class RecordTools(ToolBase):
2069
2045
  ),
2070
2046
  }
2071
2047
  )
2072
- if limit_reached:
2073
- warnings.append(
2074
- {
2075
- "code": "RECORD_ACCESS_INTERNAL_LIMIT_REACHED",
2076
- "message": "record_access reached its internal row limit before all pages were fetched.",
2077
- }
2078
- )
2079
- if timeout_reached:
2080
- warnings.append(
2081
- {
2082
- "code": "RECORD_ACCESS_TIMEOUT",
2083
- "message": "record_access reached its internal time limit before all pages were fetched.",
2084
- }
2085
- )
2086
- complete = not has_more and not limit_reached and not timeout_reached
2048
+ complete = not has_more
2087
2049
  safe_for_final_conclusion = complete and not any(
2088
2050
  warning.get("code") == "CUSTOM_VIEW_FILTER_UNVERIFIED" for warning in warnings
2089
2051
  )
@@ -5594,9 +5556,8 @@ class RecordTools(ToolBase):
5594
5556
  source_pages: list[int] = []
5595
5557
  result_amount: int | None = None
5596
5558
  has_more = False
5597
- dept_member_cache: dict[int, set[int]] = {}
5598
5559
  view_selection = resolved_view.view_selection
5599
- local_filtering = bool(filters) or bool(view_selection is not None and view_selection.conditions)
5560
+ local_filtering = bool(filters)
5600
5561
  group_stats: dict[tuple[tuple[str, object], ...], JSONObject] = {}
5601
5562
  overall_metrics = self._initialize_metric_states(metrics)
5602
5563
  matched_rows = 0
@@ -5640,13 +5601,6 @@ class RecordTools(ToolBase):
5640
5601
  continue
5641
5602
  answers = item.get("answers")
5642
5603
  answer_list = answers if isinstance(answers, list) else []
5643
- if not self._matches_view_selection(
5644
- context,
5645
- answer_list,
5646
- view_selection=view_selection,
5647
- dept_member_cache=dept_member_cache,
5648
- ):
5649
- continue
5650
5604
  if not self._matches_analyze_filters(answer_list, filters):
5651
5605
  continue
5652
5606
  matched_rows += 1
@@ -6732,7 +6686,6 @@ class RecordTools(ToolBase):
6732
6686
  if view_selection is not None
6733
6687
  else self._get_field_index(profile, context, app_key, force_refresh=False)
6734
6688
  )
6735
- dept_member_cache: dict[int, set[int]] = {}
6736
6689
  result = self._search_page(
6737
6690
  context,
6738
6691
  app_key=app_key,
@@ -6747,17 +6700,7 @@ class RecordTools(ToolBase):
6747
6700
  )
6748
6701
  rows = result.get("list")
6749
6702
  raw_rows = rows if isinstance(rows, list) else []
6750
- filtered_rows = [
6751
- item
6752
- for item in raw_rows
6753
- if isinstance(item, dict)
6754
- and self._matches_view_selection(
6755
- context,
6756
- item.get("answers") if isinstance(item.get("answers"), list) else [],
6757
- view_selection=view_selection,
6758
- dept_member_cache=dept_member_cache,
6759
- )
6760
- ]
6703
+ filtered_rows = [item for item in raw_rows if isinstance(item, dict)]
6761
6704
  if isinstance(rows, list):
6762
6705
  result = dict(result)
6763
6706
  result["list"] = filtered_rows
@@ -7007,7 +6950,6 @@ class RecordTools(ToolBase):
7007
6950
  def runner(session_profile, context):
7008
6951
  index = self._get_field_index(profile, context, app_key, force_refresh=False)
7009
6952
  view_selection = self._resolve_view_selection(profile, context, app_key, view_key=view_key, view_name=view_name)
7010
- dept_member_cache: dict[int, set[int]] = {}
7011
6953
  resolved_column_cap = _bounded_column_limit(
7012
6954
  max_columns,
7013
6955
  default_limit=MAX_LIST_COLUMN_LIMIT,
@@ -7113,13 +7055,6 @@ class RecordTools(ToolBase):
7113
7055
  continue
7114
7056
  answers = item.get("answers")
7115
7057
  answer_list = answers if isinstance(answers, list) else []
7116
- if not self._matches_view_selection(
7117
- context,
7118
- answer_list,
7119
- view_selection=view_selection,
7120
- dept_member_cache=dept_member_cache,
7121
- ):
7122
- continue
7123
7058
  matched_records += 1
7124
7059
  apply_id = _coerce_count(item.get("applyId")) or _coerce_count(item.get("id"))
7125
7060
  row = _build_flat_row(answer_list, selected_fields_from_primary, apply_id=apply_id)