@josephyan/qingflow-app-user-mcp 1.1.29 → 1.1.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@1.1.29
6
+ npm install @josephyan/qingflow-app-user-mcp@1.1.31
7
7
  ```
8
8
 
9
9
  Run:
10
10
 
11
11
  ```bash
12
- npx -y -p @josephyan/qingflow-app-user-mcp@1.1.29 qingflow-app-user-mcp
12
+ npx -y -p @josephyan/qingflow-app-user-mcp@1.1.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": "1.1.29",
3
+ "version": "1.1.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 = "1.1.29"
7
+ version = "1.1.31"
8
8
  description = "User-authenticated MCP server for Qingflow"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -1092,6 +1092,7 @@ class ViewUpsertPatch(StrictModel):
1092
1092
  columns: list[str] = Field(default_factory=list)
1093
1093
  group_by: str | None = None
1094
1094
  filters: list[ViewFilterRulePatch] = Field(default_factory=list)
1095
+ filter_groups: list[list[ViewFilterRulePatch]] = Field(default_factory=list, validation_alias=AliasChoices("filter_groups", "filterGroups", "or_filters", "orFilters"))
1095
1096
  start_field: str | None = Field(default=None, validation_alias=AliasChoices("start_field", "startField"))
1096
1097
  end_field: str | None = Field(default=None, validation_alias=AliasChoices("end_field", "endField"))
1097
1098
  title_field: str | None = Field(default=None, validation_alias=AliasChoices("title_field", "titleField"))
@@ -1131,6 +1132,12 @@ class ViewUpsertPatch(StrictModel):
1131
1132
  payload["filters"] = payload.pop("filter_rules")
1132
1133
  if "filterRules" in payload and "filters" not in payload:
1133
1134
  payload["filters"] = payload.pop("filterRules")
1135
+ if "filterGroups" in payload and "filter_groups" not in payload:
1136
+ payload["filter_groups"] = payload.pop("filterGroups")
1137
+ if "or_filters" in payload and "filter_groups" not in payload:
1138
+ payload["filter_groups"] = payload.pop("or_filters")
1139
+ if "orFilters" in payload and "filter_groups" not in payload:
1140
+ payload["filter_groups"] = payload.pop("orFilters")
1134
1141
  if "queryConditions" in payload and "query_conditions" not in payload:
1135
1142
  payload["query_conditions"] = payload.pop("queryConditions")
1136
1143
  if "query_condition" in payload and "query_conditions" not in payload:
@@ -7181,7 +7181,11 @@ class AiBuilderFacade:
7181
7181
  missing_gantt_fields = [name for name in (start_field, end_field, title_field) if name and name not in field_names]
7182
7182
  if missing_gantt_fields:
7183
7183
  blocking_issues.append({"error_code": "UNKNOWN_VIEW_FIELD", "view_name": patch.get("name"), "missing_fields": missing_gantt_fields})
7184
- translated_filters, filter_issues = _build_view_filter_groups(current_fields_by_name=current_fields_by_name, filters=patch.get("filters") or [])
7184
+ translated_filters, filter_issues = _build_view_filter_groups(
7185
+ current_fields_by_name=current_fields_by_name,
7186
+ filters=patch.get("filters") or [],
7187
+ filter_groups=patch.get("filter_groups") or [],
7188
+ )
7185
7189
  if filter_issues:
7186
7190
  blocking_issues.extend(
7187
7191
  {
@@ -8924,7 +8928,11 @@ class AiBuilderFacade:
8924
8928
  translated_filters = None
8925
8929
  filter_issues = []
8926
8930
  else:
8927
- translated_filters, filter_issues = _build_view_filter_groups(current_fields_by_name=current_fields_by_name, filters=patch.filters)
8931
+ translated_filters, filter_issues = _build_view_filter_groups(
8932
+ current_fields_by_name=current_fields_by_name,
8933
+ filters=patch.filters,
8934
+ filter_groups=patch.filter_groups,
8935
+ )
8928
8936
  if filter_issues:
8929
8937
  first_issue = filter_issues[0]
8930
8938
  return _failed(
@@ -24069,31 +24077,51 @@ def _build_view_filter_groups(
24069
24077
  *,
24070
24078
  current_fields_by_name: dict[str, dict[str, Any]],
24071
24079
  filters: list[Any],
24080
+ filter_groups: list[list[Any]] | None = None,
24072
24081
  ) -> tuple[list[list[dict[str, Any]]], list[dict[str, Any]]]:
24073
- translated_rules: list[dict[str, Any]] = []
24074
24082
  issues: list[dict[str, Any]] = []
24075
- for raw_rule in filters:
24076
- if hasattr(raw_rule, "model_dump"):
24077
- raw_rule = raw_rule.model_dump(mode="json")
24078
- if not isinstance(raw_rule, dict):
24079
- continue
24080
- field_name = str(raw_rule.get("field_name") or "").strip()
24081
- field = current_fields_by_name.get(field_name)
24082
- if field is None:
24083
+ raw_groups: list[list[Any]] = []
24084
+ if filters:
24085
+ raw_groups.append(list(filters))
24086
+ for raw_group in filter_groups or []:
24087
+ if not isinstance(raw_group, list):
24083
24088
  issues.append(
24084
24089
  {
24085
- "error_code": "UNKNOWN_VIEW_FIELD",
24086
- "missing_fields": [field_name] if field_name else [],
24087
- "reason_path": "filters[].field_name",
24090
+ "error_code": "INVALID_VIEW_FILTER_GROUP",
24091
+ "reason_path": "filter_groups[]",
24092
+ "message": "filter_groups must be a list of filter rule lists",
24088
24093
  }
24089
24094
  )
24090
24095
  continue
24091
- translated_rule, issue = _translate_view_filter_rule(field=field, rule=raw_rule)
24092
- if issue:
24093
- issues.append(issue)
24094
- continue
24095
- translated_rules.append(translated_rule)
24096
- return ([translated_rules] if translated_rules else []), issues
24096
+ raw_groups.append(raw_group)
24097
+
24098
+ translated_groups: list[list[dict[str, Any]]] = []
24099
+ for group_index, raw_group in enumerate(raw_groups):
24100
+ translated_rules: list[dict[str, Any]] = []
24101
+ for raw_rule in raw_group:
24102
+ if hasattr(raw_rule, "model_dump"):
24103
+ raw_rule = raw_rule.model_dump(mode="json")
24104
+ if not isinstance(raw_rule, dict):
24105
+ continue
24106
+ field_name = str(raw_rule.get("field_name") or "").strip()
24107
+ field = current_fields_by_name.get(field_name)
24108
+ if field is None:
24109
+ issues.append(
24110
+ {
24111
+ "error_code": "UNKNOWN_VIEW_FIELD",
24112
+ "missing_fields": [field_name] if field_name else [],
24113
+ "reason_path": f"filter_groups[{group_index}].field_name" if filter_groups else "filters[].field_name",
24114
+ }
24115
+ )
24116
+ continue
24117
+ translated_rule, issue = _translate_view_filter_rule(field=field, rule=raw_rule)
24118
+ if issue:
24119
+ issues.append(issue)
24120
+ continue
24121
+ translated_rules.append(translated_rule)
24122
+ if translated_rules:
24123
+ translated_groups.append(translated_rules)
24124
+ return translated_groups, issues
24097
24125
 
24098
24126
 
24099
24127
  def _translate_view_filter_rule(*, field: dict[str, Any], rule: dict[str, Any]) -> tuple[dict[str, Any], dict[str, Any] | None]:
@@ -24388,7 +24416,7 @@ def _public_view_filter_values_from_rule(rule: dict[str, Any]) -> list[str]:
24388
24416
 
24389
24417
 
24390
24418
  def _public_view_filter_operator_from_judge_type(judge_type: Any, *, values: list[str]) -> str:
24391
- numeric = _coerce_positive_int(judge_type)
24419
+ numeric = _coerce_nonnegative_int(judge_type)
24392
24420
  if numeric == JUDGE_EQUAL:
24393
24421
  return "eq" if values else "is_empty"
24394
24422
  if numeric == JUDGE_UNEQUAL:
@@ -5763,6 +5763,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
5763
5763
  "upsert_views[].type",
5764
5764
  "upsert_views[].columns",
5765
5765
  "upsert_views[].filters",
5766
+ "upsert_views[].filter_groups",
5766
5767
  "upsert_views[].buttons",
5767
5768
  "upsert_views[].visibility",
5768
5769
  "upsert_views[].query_conditions",
@@ -5781,6 +5782,9 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
5781
5782
  "kanban": "board",
5782
5783
  "filter_rules": "filters",
5783
5784
  "filterRules": "filters",
5785
+ "filterGroups": "filter_groups",
5786
+ "or_filters": "filter_groups",
5787
+ "orFilters": "filter_groups",
5784
5788
  "queryConditions": "query_conditions",
5785
5789
  "query_condition": "query_conditions",
5786
5790
  "queryCondition": "query_conditions",
@@ -5807,7 +5811,8 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
5807
5811
  },
5808
5812
  "execution_notes": [
5809
5813
  "upsert_views[].visibility may set per-view visibility; omit it to preserve an existing view's auth or default a new view to workspace/not",
5810
- "filters are saved fixed filters that apply when the view opens; query_conditions configure the frontend query panel and only apply after a user enters query values",
5814
+ "filters are saved fixed filters that apply when the view opens; filters is one AND group, filter_groups is OR across groups with AND inside each group",
5815
+ "query_conditions configure the frontend query panel and only apply after a user enters query values",
5811
5816
  "upsert_views[].query_conditions.rows is a layout matrix of field names; it is compiled to backend queryCondition queIds",
5812
5817
  "use patch_views for partial parameter replacement on existing views; the tool reads current config, merges patch_views[].set/unset, then submits the backend full-save payload internally",
5813
5818
  "remove_views accepts a raw view_key or an exact unique view name; after DELETE the tool verifies deletion by single view_key readback, not by a full app view list",
@@ -5891,6 +5896,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
5891
5896
  "views[].type",
5892
5897
  "views[].columns",
5893
5898
  "views[].filters",
5899
+ "views[].filter_groups",
5894
5900
  "views[].query_conditions",
5895
5901
  "views[].visibility",
5896
5902
  "views[].set",
@@ -5908,6 +5914,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
5908
5914
  "upsert_views[].type",
5909
5915
  "upsert_views[].columns",
5910
5916
  "upsert_views[].filters",
5917
+ "upsert_views[].filter_groups",
5911
5918
  "upsert_views[].buttons",
5912
5919
  "upsert_views[].visibility",
5913
5920
  "upsert_views[].query_conditions",
@@ -5926,6 +5933,9 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
5926
5933
  "kanban": "board",
5927
5934
  "filter_rules": "filters",
5928
5935
  "filterRules": "filters",
5936
+ "filterGroups": "filter_groups",
5937
+ "or_filters": "filter_groups",
5938
+ "orFilters": "filter_groups",
5929
5939
  "queryConditions": "query_conditions",
5930
5940
  "query_condition": "query_conditions",
5931
5941
  "queryCondition": "query_conditions",
@@ -5968,7 +5978,8 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
5968
5978
  "views[].action_buttons are applied after the target view_key is resolved; action_buttons require publish=true because the button writer publishes button configuration",
5969
5979
  "buttons omitted preserves existing button config; buttons=[] clears all buttons; buttons=[...] replaces the full button config",
5970
5980
  "upsert_views[].visibility may set per-view visibility; omit it to preserve an existing view's auth or default a new view to workspace/not",
5971
- "filters are saved fixed filters that apply when the view opens; query_conditions configure the frontend query panel and only apply after a user enters query values",
5981
+ "filters are saved fixed filters that apply when the view opens; filters is one AND group, filter_groups is OR across groups with AND inside each group",
5982
+ "query_conditions configure the frontend query panel and only apply after a user enters query values",
5972
5983
  "upsert_views[].query_conditions.rows is a layout matrix of field names; it is compiled to backend queryCondition queIds",
5973
5984
  "use patch_views for partial parameter replacement on existing views; the public update mode is patch even though the backend save is still a full view payload",
5974
5985
  "remove_views accepts a raw view_key or an exact unique view name; after DELETE the tool verifies deletion by single view_key readback, not by a full app view list",