@josephyan/qingflow-app-user-mcp 0.2.0-beta.30 → 0.2.0-beta.31

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.30
6
+ npm install @josephyan/qingflow-app-user-mcp@0.2.0-beta.31
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.30 qingflow-app-user-mcp
12
+ npx -y -p @josephyan/qingflow-app-user-mcp@0.2.0-beta.31 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.30",
3
+ "version": "0.2.0-beta.31",
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.0b30"
7
+ version = "0.2.0b31"
8
8
  description = "User-authenticated MCP server for Qingflow"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -60,7 +60,9 @@ Use `record_member_candidates` / `record_department_candidates` as the default l
60
60
  ## Record Read Rules
61
61
 
62
62
  - Use `record_list` for browse/export/sample inspection only
63
- - For `columns`, prefer `[{ "field_id": 12 }]`; bare integer field ids are accepted for compatibility
63
+ - For `columns`, use `[{ "field_id": 12 }]`
64
+ - For `where`, use `{ "field_id": 12, "op": "eq", "value": "进行中" }`
65
+ - For `order_by`, use `{ "field_id": 18, "direction": "desc" }`
64
66
  - Use `record_get` when `record_id` is known
65
67
  - `record_get` without explicit `columns` still returns only applicant-node visible fields; do not assume it exposes the full builder-side record
66
68
  - `record_list` and `record_get` may reject hidden-field `field_id`s because record tools now validate against the applicant-node visible schema only
@@ -15,9 +15,9 @@ Remember that `record_schema_get` only exposes the current user's applicant-node
15
15
 
16
16
  Keep the browse DSL simple:
17
17
 
18
- - `columns`: prefer `[{ "field_id": 12 }]`; bare integers are accepted for compatibility
19
- - `where`: flat AND filters only
20
- - `order_by`: field sorting only
18
+ - `columns`: use `[{ "field_id": 12 }]`
19
+ - `where`: flat AND filters only, using `{ "field_id": 12, "op": "eq", "value": "进行中" }`
20
+ - `order_by`: field sorting only, using `{ "field_id": 18, "direction": "desc" }`
21
21
  - `limit` and `page`: browsing intent only
22
22
 
23
23
  Do not use `record_list` for grouped conclusions, ratios, rankings, trends, or any final statistical claim.
@@ -2,4 +2,4 @@ from __future__ import annotations
2
2
 
3
3
  __all__ = ["__version__"]
4
4
 
5
- __version__ = "0.2.0b30"
5
+ __version__ = "0.2.0b31"
@@ -78,7 +78,10 @@ Analysis answers must include concrete numbers. When applicable, include percent
78
78
 
79
79
  `record_schema_get -> record_list / record_get / record_write`
80
80
 
81
- - For `columns`, prefer `[{{field_id}}]`; bare integer field ids remain supported for compatibility.
81
+ - Use `columns` as `[{{field_id}}]`
82
+ - Use `where` items as `{{field_id, op, value}}`
83
+ - Use `order_by` items as `{{field_id, direction}}`
84
+ - Legacy forms such as bare integer `field_id`, `fieldId`, `operator`, `values`, or `order` may still parse, but they are compatibility-only and not the canonical DSL
82
85
 
83
86
  `record_write` uses SQL-like JSON clauses:
84
87
 
@@ -66,7 +66,10 @@ Analysis answers must include concrete numbers. When applicable, include percent
66
66
 
67
67
  `record_schema_get -> record_list / record_get / record_write`
68
68
 
69
- - For `columns`, prefer `[{{field_id}}]`; bare integer field ids remain supported for compatibility.
69
+ - Use `columns` as `[{{field_id}}]`
70
+ - Use `where` items as `{{field_id, op, value}}`
71
+ - Use `order_by` items as `{{field_id, direction}}`
72
+ - Legacy forms such as bare integer `field_id`, `fieldId`, `operator`, `values`, or `order` may still parse, but they are compatibility-only and not the canonical DSL
70
73
 
71
74
  `record_write` uses SQL-like JSON clauses:
72
75
 
@@ -565,6 +565,12 @@ class RecordTools(ToolBase):
565
565
  raise_tool_error(QingflowApiError.config_error("app_key is required"))
566
566
  if limit <= 0:
567
567
  raise_tool_error(QingflowApiError.config_error("limit must be positive"))
568
+ legacy_warnings = _detect_analyze_legacy_warnings(
569
+ dimensions=dimensions,
570
+ metrics=metrics,
571
+ filters=filters,
572
+ sort=sort,
573
+ )
568
574
 
569
575
  def runner(session_profile, context):
570
576
  index = self._get_field_index(profile, context, app_key, force_refresh=False)
@@ -594,6 +600,7 @@ class RecordTools(ToolBase):
594
600
  limit=limit,
595
601
  strict_full=strict_full,
596
602
  output_profile=output_profile,
603
+ extra_warnings=legacy_warnings,
597
604
  )
598
605
 
599
606
  return self._run_record_tool(profile, runner)
@@ -615,6 +622,7 @@ class RecordTools(ToolBase):
615
622
  normalized_output_profile = self._normalize_public_output_profile(output_profile)
616
623
  if not app_key:
617
624
  raise_tool_error(QingflowApiError.config_error("app_key is required"))
625
+ legacy_warnings = _detect_record_list_legacy_warnings(columns=columns, where=where, order_by=order_by)
618
626
  normalized_columns = _normalize_public_column_selectors(columns)
619
627
  if not normalized_columns:
620
628
  raise_tool_error(QingflowApiError.config_error("columns is required"))
@@ -651,6 +659,7 @@ class RecordTools(ToolBase):
651
659
  list_data = cast(JSONObject, cast(JSONObject, raw["data"])["list"])
652
660
  pagination = cast(JSONObject, list_data["pagination"])
653
661
  warnings: list[JSONObject] = []
662
+ warnings.extend(legacy_warnings)
654
663
  warning = _normalize_optional_text(list_data.get("analysis_warning"))
655
664
  if warning:
656
665
  warnings.append({"code": "BROWSE_ONLY", "message": warning})
@@ -1906,6 +1915,7 @@ class RecordTools(ToolBase):
1906
1915
  limit: int,
1907
1916
  strict_full: bool,
1908
1917
  output_profile: str,
1918
+ extra_warnings: list[JSONObject] | None = None,
1909
1919
  ) -> JSONObject:
1910
1920
  started_at = time.perf_counter()
1911
1921
  analysis_paging = _fixed_analysis_scan_policy()
@@ -2068,7 +2078,11 @@ class RecordTools(ToolBase):
2068
2078
  for idx, row in enumerate(rows, start=1)
2069
2079
  ]
2070
2080
 
2071
- warnings = self._build_analyze_warnings(local_filtering=local_filtering, rows_truncated=rows_truncated)
2081
+ warnings = self._build_analyze_warnings(
2082
+ local_filtering=local_filtering,
2083
+ rows_truncated=rows_truncated,
2084
+ extra_warnings=extra_warnings or [],
2085
+ )
2072
2086
  completeness: JSONObject = {
2073
2087
  "status": completeness_status,
2074
2088
  "safe_for_final_conclusion": completeness_status == "complete",
@@ -2230,8 +2244,15 @@ class RecordTools(ToolBase):
2230
2244
  )
2231
2245
  return sorted_rows
2232
2246
 
2233
- def _build_analyze_warnings(self, *, local_filtering: bool, rows_truncated: bool) -> list[JSONObject]:
2247
+ def _build_analyze_warnings(
2248
+ self,
2249
+ *,
2250
+ local_filtering: bool,
2251
+ rows_truncated: bool,
2252
+ extra_warnings: list[JSONObject],
2253
+ ) -> list[JSONObject]:
2234
2254
  warnings: list[JSONObject] = []
2255
+ warnings.extend(extra_warnings)
2235
2256
  if local_filtering:
2236
2257
  warnings.append({"code": "LOCAL_VIEW_FILTERING"})
2237
2258
  if rows_truncated:
@@ -3734,18 +3755,23 @@ class RecordTools(ToolBase):
3734
3755
  for idx, item in enumerate(where):
3735
3756
  if not isinstance(item, dict):
3736
3757
  raise_tool_error(QingflowApiError.config_error(f"where[{idx}] must be an object"))
3758
+ _ensure_allowed_record_list_keys(
3759
+ item,
3760
+ location=f"where[{idx}]",
3761
+ allowed_keys={"field_id", "fieldId", "op", "operator", "value", "values"},
3762
+ example="{'field_id': 12, 'op': 'eq', 'value': '进行中'}",
3763
+ )
3737
3764
  field_id = _coerce_count(item.get("field_id", item.get("fieldId")))
3738
3765
  if field_id is None:
3739
3766
  raise_tool_error(QingflowApiError.config_error(f"where[{idx}] requires field_id"))
3740
3767
  payload: JSONObject = {"field_id": field_id}
3741
- if "op" in item:
3742
- payload["op"] = item["op"]
3743
- if "operator" in item:
3744
- payload["operator"] = item["operator"]
3768
+ op = item.get("op", item.get("operator"))
3769
+ if op is not None:
3770
+ payload["op"] = op
3745
3771
  if "value" in item:
3746
3772
  payload["value"] = item["value"]
3747
3773
  elif "values" in item:
3748
- payload["values"] = item["values"]
3774
+ payload["value"] = item["values"]
3749
3775
  normalized.append(payload)
3750
3776
  return normalized
3751
3777
 
@@ -3754,6 +3780,12 @@ class RecordTools(ToolBase):
3754
3780
  for idx, item in enumerate(order_by):
3755
3781
  if not isinstance(item, dict):
3756
3782
  raise_tool_error(QingflowApiError.config_error(f"order_by[{idx}] must be an object"))
3783
+ _ensure_allowed_record_list_keys(
3784
+ item,
3785
+ location=f"order_by[{idx}]",
3786
+ allowed_keys={"field_id", "fieldId", "direction", "order"},
3787
+ example="{'field_id': 18, 'direction': 'desc'}",
3788
+ )
3757
3789
  field_id = _coerce_count(item.get("field_id", item.get("fieldId")))
3758
3790
  if field_id is None:
3759
3791
  raise_tool_error(QingflowApiError.config_error(f"order_by[{idx}] requires field_id"))
@@ -5361,6 +5393,12 @@ def _normalize_public_column_selectors(columns: list[JSONObject | int]) -> list[
5361
5393
  if isinstance(item, int):
5362
5394
  field_id = item
5363
5395
  elif isinstance(item, dict):
5396
+ _ensure_allowed_record_list_keys(
5397
+ item,
5398
+ location="columns[]",
5399
+ allowed_keys={"field_id", "fieldId"},
5400
+ example="{'field_id': 12}",
5401
+ )
5364
5402
  field_id = _coerce_count(item.get("field_id", item.get("fieldId")))
5365
5403
  if field_id is None or field_id < 0:
5366
5404
  raise_tool_error(
@@ -5376,6 +5414,92 @@ def _column_selector_payload(field_id: int) -> JSONObject:
5376
5414
  return {"field_id": field_id}
5377
5415
 
5378
5416
 
5417
+ def _ensure_allowed_record_list_keys(
5418
+ item: JSONObject,
5419
+ *,
5420
+ location: str,
5421
+ allowed_keys: set[str],
5422
+ example: str,
5423
+ ) -> None:
5424
+ unexpected_keys = sorted(str(key) for key in item.keys() if str(key) not in allowed_keys)
5425
+ if unexpected_keys:
5426
+ raise_tool_error(
5427
+ QingflowApiError.config_error(
5428
+ f"{location} contains unsupported keys: {unexpected_keys}. Use {example}."
5429
+ )
5430
+ )
5431
+
5432
+
5433
+ def _detect_record_list_legacy_warnings(
5434
+ *,
5435
+ columns: list[JSONObject | int],
5436
+ where: list[JSONObject],
5437
+ order_by: list[JSONObject],
5438
+ ) -> list[JSONObject]:
5439
+ warnings: list[JSONObject] = []
5440
+ if any(isinstance(item, int) or (isinstance(item, dict) and "fieldId" in item) for item in columns):
5441
+ warnings.append(
5442
+ {
5443
+ "code": "LEGACY_LIST_COLUMNS_DSL",
5444
+ "message": "Use columns as [{field_id}] objects. Bare integers and fieldId are compatibility-only.",
5445
+ }
5446
+ )
5447
+ if any(isinstance(item, dict) and any(key in item for key in ("fieldId", "operator", "values")) for item in where):
5448
+ warnings.append(
5449
+ {
5450
+ "code": "LEGACY_LIST_FILTER_DSL",
5451
+ "message": "Use where items as {field_id, op, value}. fieldId/operator/values are compatibility-only.",
5452
+ }
5453
+ )
5454
+ if any(isinstance(item, dict) and any(key in item for key in ("fieldId", "order")) for item in order_by):
5455
+ warnings.append(
5456
+ {
5457
+ "code": "LEGACY_LIST_SORT_DSL",
5458
+ "message": "Use order_by items as {field_id, direction}. fieldId/order are compatibility-only.",
5459
+ }
5460
+ )
5461
+ return warnings
5462
+
5463
+
5464
+ def _detect_analyze_legacy_warnings(
5465
+ *,
5466
+ dimensions: list[JSONObject],
5467
+ metrics: list[JSONObject],
5468
+ filters: list[JSONObject],
5469
+ sort: list[JSONObject],
5470
+ ) -> list[JSONObject]:
5471
+ warnings: list[JSONObject] = []
5472
+ if any(isinstance(item, dict) and "fieldId" in item for item in dimensions):
5473
+ warnings.append(
5474
+ {
5475
+ "code": "LEGACY_ANALYZE_DIMENSION_DSL",
5476
+ "message": "Use dimensions as {field_id, alias, bucket}. fieldId is compatibility-only.",
5477
+ }
5478
+ )
5479
+ if any(isinstance(item, dict) and any(key in item for key in ("fieldId", "type", "agg", "aggregation")) for item in metrics):
5480
+ warnings.append(
5481
+ {
5482
+ "code": "LEGACY_ANALYZE_METRIC_DSL",
5483
+ "message": "Use metrics as {op, field_id, alias}. fieldId/type/agg/aggregation are compatibility-only.",
5484
+ }
5485
+ )
5486
+ if any(isinstance(item, dict) and any(key in item for key in ("fieldId", "operator", "values")) for item in filters):
5487
+ warnings.append(
5488
+ {
5489
+ "code": "LEGACY_ANALYZE_FILTER_DSL",
5490
+ "message": "Use filters as {field_id, op, value}. fieldId/operator/values are compatibility-only.",
5491
+ }
5492
+ )
5493
+ if any(isinstance(item, dict) and "direction" in item for item in sort):
5494
+ warnings.append(
5495
+ {
5496
+ "code": "LEGACY_ANALYZE_SORT_DSL",
5497
+ "message": "Use sort items as {by, order}. direction is compatibility-only.",
5498
+ }
5499
+ )
5500
+ return warnings
5501
+
5502
+
5379
5503
  def _resolve_sort_ascend(item: JSONObject) -> bool:
5380
5504
  if "isAscend" in item:
5381
5505
  return bool(item["isAscend"])