@josephyan/qingflow-app-user-mcp 0.2.0-beta.27 → 0.2.0-beta.28

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.27
6
+ npm install @josephyan/qingflow-app-user-mcp@0.2.0-beta.28
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.27 qingflow-app-user-mcp
12
+ npx -y -p @josephyan/qingflow-app-user-mcp@0.2.0-beta.28 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.27",
3
+ "version": "0.2.0-beta.28",
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.0b27"
7
+ version = "0.2.0b28"
8
8
  description = "User-authenticated MCP server for Qingflow"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -9,7 +9,7 @@ metadata:
9
9
 
10
10
  This skill is for final statistical conclusions only.
11
11
  Assumes MCP is connected, authenticated, and on the correct workspace.
12
- Analysis tasks must start with `record_schema_get`. Use field_id-based DSLs only.
12
+ Analysis tasks must start with `record_schema_get`. Read top-level `fields` and `suggested_*`, then build field_id-based DSLs only.
13
13
 
14
14
  ## Step 1: `record_schema_get` → Step 2: build DSL → Step 3: `record_analyze`
15
15
 
@@ -27,7 +27,7 @@ Use exactly one of these default paths:
27
27
  - `record_get`
28
28
  - `record_write`
29
29
 
30
- `record_schema_get` only exposes the current user's applicant-node visible fields; if a field is missing, treat it as unavailable in the current permission scope.
30
+ `record_schema_get` only exposes the current user's applicant-node visible fields. Read top-level `fields` and `suggested_*`; if a field is missing, treat it as unavailable in the current permission scope.
31
31
 
32
32
  ## Supporting Tools
33
33
 
@@ -58,6 +58,7 @@ Use exactly one of these default paths:
58
58
  ## Record Read Rules
59
59
 
60
60
  - Use `record_list` for browse/export/sample inspection only
61
+ - For `columns`, prefer `[{ "field_id": 12 }]`; bare integer field ids are accepted for compatibility
61
62
  - Use `record_get` when `record_id` is known
62
63
  - `record_get` without explicit `columns` still returns only applicant-node visible fields; do not assume it exposes the full builder-side record
63
64
  - `record_list` and `record_get` may reject hidden-field `field_id`s because record tools now validate against the applicant-node visible schema only
@@ -103,6 +104,13 @@ The DSL is clause-shaped like SQL, but it is **not raw SQL text**.
103
104
  - Do not assume department names resolve automatically; `record_department_candidates` returns ids, but direct name-to-id write is not automatic
104
105
  - Do not assume `record_schema_get` is a builder/full-field schema.
105
106
 
107
+ ### Complex field quick examples
108
+
109
+ - `member`: `✅ {"field_id":5,"value":{"id":7}}` / `❌ {"field_id":5,"value":"张三"}`
110
+ - `department`: `✅ {"field_id":22,"value":{"id":336193,"value":"北斗组"}}` / `❌ {"field_id":22,"value":"北斗组"}`
111
+ - `relation`: `✅ {"field_id":25,"value":{"apply_id":5001}}` / `❌ {"field_id":25,"value":"客户A"}`
112
+ - `attachment`: upload first, then `✅ {"field_id":13,"value":{"value":"https://.../a.pdf","name":"a.pdf"}}` / `❌ {"field_id":13,"value":"/tmp/a.pdf"}`
113
+
106
114
  ## Response Interpretation
107
115
 
108
116
  - `record_list` returns browse/sample data, not final analysis conclusions
@@ -15,7 +15,7 @@ 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`: field ids only
18
+ - `columns`: prefer `[{ "field_id": 12 }]`; bare integers are accepted for compatibility
19
19
  - `where`: flat AND filters only
20
20
  - `order_by`: field sorting only
21
21
  - `limit` and `page`: browsing intent only
@@ -110,3 +110,21 @@ If the payload includes them, stop after the blocked `record_write` response and
110
110
  - Relation fields are record-id based. Resolve the referenced target first, then write the relation field with the real `record_id`.
111
111
  - Attachment fields are two-step: upload first with `file_upload_local`, then reuse the returned attachment payload in `record_write`.
112
112
  - Subtable writes require the current schema shape; when updating existing subtable rows, preserve row ids if the current record exposes them.
113
+
114
+ ### Quick field examples
115
+
116
+ ```json
117
+ { "field_id": 5, "value": { "id": 7 } }
118
+ ```
119
+
120
+ ```json
121
+ { "field_id": 22, "value": { "id": 336193, "value": "北斗组" } }
122
+ ```
123
+
124
+ ```json
125
+ { "field_id": 25, "value": { "apply_id": 5001 } }
126
+ ```
127
+
128
+ ```json
129
+ { "field_id": 13, "value": { "value": "https://.../a.pdf", "name": "a.pdf" } }
130
+ ```
@@ -2,4 +2,4 @@ from __future__ import annotations
2
2
 
3
3
  __all__ = ["__version__"]
4
4
 
5
- __version__ = "0.2.0b27"
5
+ __version__ = "0.2.0b28"
@@ -53,6 +53,7 @@ Always call `record_schema_get` before `record_list`, `record_get`, `record_writ
53
53
 
54
54
  - Hidden fields are omitted.
55
55
  - Missing fields mean the field is not visible in the current permission scope.
56
+ - Read `fields` and `suggested_*` from the top level of the schema response.
56
57
 
57
58
  ## Analytics Path
58
59
 
@@ -79,6 +80,8 @@ Analysis answers must include concrete numbers. When applicable, include percent
79
80
 
80
81
  `record_schema_get -> record_list / record_get / record_write`
81
82
 
83
+ - For `columns`, prefer `[{{field_id}}]`; bare integer field ids remain supported for compatibility.
84
+
82
85
  `record_write` uses SQL-like JSON clauses:
83
86
 
84
87
  - `insert` -> `values`
@@ -41,6 +41,7 @@ Always call `record_schema_get` before `record_list`, `record_get`, `record_writ
41
41
 
42
42
  - Hidden fields are omitted.
43
43
  - Missing fields mean the field is not visible in the current permission scope.
44
+ - Read `fields` and `suggested_*` from the top level of the schema response.
44
45
 
45
46
  ## Analytics Path
46
47
 
@@ -67,6 +68,8 @@ Analysis answers must include concrete numbers. When applicable, include percent
67
68
 
68
69
  `record_schema_get -> record_list / record_get / record_write`
69
70
 
71
+ - For `columns`, prefer `[{{field_id}}]`; bare integer field ids remain supported for compatibility.
72
+
70
73
  `record_write` uses SQL-like JSON clauses:
71
74
 
72
75
  - `insert` -> `values`
@@ -269,7 +269,7 @@ class RecordTools(ToolBase):
269
269
  def record_list(
270
270
  profile: str = DEFAULT_PROFILE,
271
271
  app_key: str = "",
272
- columns: list[int] | None = None,
272
+ columns: list[JSONObject | int] | None = None,
273
273
  where: list[JSONObject] | None = None,
274
274
  order_by: list[JSONObject] | None = None,
275
275
  limit: int = 50,
@@ -296,7 +296,7 @@ class RecordTools(ToolBase):
296
296
  profile: str = DEFAULT_PROFILE,
297
297
  app_key: str = "",
298
298
  record_id: int = 0,
299
- columns: list[int] | None = None,
299
+ columns: list[JSONObject | int] | None = None,
300
300
  workflow_node_id: int | None = None,
301
301
  output_profile: str = "normal",
302
302
  ) -> JSONObject:
@@ -389,23 +389,21 @@ class RecordTools(ToolBase):
389
389
  "ok": True,
390
390
  "status": "success",
391
391
  "request_route": self._request_route_payload(context),
392
- "data": {
393
- "app_key": app_key,
394
- "schema_scope": "applicant_node",
395
- "workflow_node": {
396
- "workflow_node_id": applicant_node.workflow_node_id,
397
- "name": applicant_node.name,
398
- "type": applicant_node.type,
399
- },
400
- "view_resolution": _view_selection_payload(view_selection),
401
- "fields": fields,
402
- "suggested_dimensions": suggested_dimensions,
403
- "suggested_metrics": suggested_metrics,
404
- "suggested_time_fields": suggested_time_fields,
392
+ "app_key": app_key,
393
+ "schema_scope": "applicant_node",
394
+ "workflow_node": {
395
+ "workflow_node_id": applicant_node.workflow_node_id,
396
+ "name": applicant_node.name,
397
+ "type": applicant_node.type,
405
398
  },
399
+ "view_resolution": _view_selection_payload(view_selection),
400
+ "fields": fields,
401
+ "suggested_dimensions": suggested_dimensions,
402
+ "suggested_metrics": suggested_metrics,
403
+ "suggested_time_fields": suggested_time_fields,
406
404
  }
407
405
  if output_profile == "verbose":
408
- response["data"]["field_count"] = len(fields)
406
+ response["field_count"] = len(fields)
409
407
  return response
410
408
 
411
409
  return self._run_record_tool(profile, runner)
@@ -605,7 +603,7 @@ class RecordTools(ToolBase):
605
603
  *,
606
604
  profile: str,
607
605
  app_key: str,
608
- columns: list[int],
606
+ columns: list[JSONObject | int],
609
607
  where: list[JSONObject],
610
608
  order_by: list[JSONObject],
611
609
  limit: int,
@@ -617,10 +615,9 @@ class RecordTools(ToolBase):
617
615
  normalized_output_profile = self._normalize_public_output_profile(output_profile)
618
616
  if not app_key:
619
617
  raise_tool_error(QingflowApiError.config_error("app_key is required"))
620
- if not columns:
618
+ normalized_columns = _normalize_public_column_selectors(columns)
619
+ if not normalized_columns:
621
620
  raise_tool_error(QingflowApiError.config_error("columns is required"))
622
- if any(not isinstance(item, int) or item < 0 for item in columns):
623
- raise_tool_error(QingflowApiError.config_error("columns must be a list of field_id integers"))
624
621
  if limit <= 0:
625
622
  raise_tool_error(QingflowApiError.config_error("limit must be positive"))
626
623
  if page <= 0:
@@ -640,8 +637,8 @@ class RecordTools(ToolBase):
640
637
  filters=self._normalize_record_list_where(where),
641
638
  sorts=self._normalize_record_list_order_by(order_by),
642
639
  max_rows=limit,
643
- max_columns=len(columns),
644
- select_columns=columns,
640
+ max_columns=len(normalized_columns),
641
+ select_columns=normalized_columns,
645
642
  amount_column=None,
646
643
  time_range={},
647
644
  stat_policy={},
@@ -674,7 +671,7 @@ class RecordTools(ToolBase):
674
671
  "result_amount": pagination.get("result_amount"),
675
672
  },
676
673
  "selection": {
677
- "columns": columns,
674
+ "columns": [_column_selector_payload(field_id) for field_id in normalized_columns],
678
675
  "view": cast(JSONObject, raw["data"]).get("view"),
679
676
  },
680
677
  },
@@ -695,17 +692,16 @@ class RecordTools(ToolBase):
695
692
  profile: str,
696
693
  app_key: str,
697
694
  record_id: int,
698
- columns: list[int],
695
+ columns: list[JSONObject | int],
699
696
  workflow_node_id: int | None,
700
697
  output_profile: str,
701
698
  ) -> JSONObject:
702
699
  normalized_output_profile = self._normalize_public_output_profile(output_profile)
703
700
  if record_id <= 0:
704
701
  raise_tool_error(QingflowApiError.config_error("record_id must be positive"))
705
- if columns and any(not isinstance(item, int) or item < 0 for item in columns):
706
- raise_tool_error(QingflowApiError.config_error("columns must be a list of field_id integers"))
702
+ normalized_columns = _normalize_public_column_selectors(columns)
707
703
 
708
- if columns:
704
+ if normalized_columns:
709
705
  raw = self.record_query(
710
706
  profile=profile,
711
707
  query_mode="record",
@@ -720,8 +716,8 @@ class RecordTools(ToolBase):
720
716
  filters=[],
721
717
  sorts=[],
722
718
  max_rows=1,
723
- max_columns=len(columns),
724
- select_columns=columns,
719
+ max_columns=len(normalized_columns),
720
+ select_columns=normalized_columns,
725
721
  amount_column=None,
726
722
  time_range={},
727
723
  stat_policy={},
@@ -742,7 +738,7 @@ class RecordTools(ToolBase):
742
738
  "record_id": record_id,
743
739
  "record": record_data.get("row"),
744
740
  "selection": {
745
- "columns": columns,
741
+ "columns": [_column_selector_payload(field_id) for field_id in normalized_columns],
746
742
  "workflow_node_id": workflow_node_id,
747
743
  },
748
744
  },
@@ -5262,6 +5258,28 @@ def _extract_sort_selector(item: JSONObject) -> JSONValue:
5262
5258
  return None
5263
5259
 
5264
5260
 
5261
+ def _normalize_public_column_selectors(columns: list[JSONObject | int]) -> list[int]:
5262
+ normalized: list[int] = []
5263
+ for item in columns:
5264
+ field_id: int | None = None
5265
+ if isinstance(item, int):
5266
+ field_id = item
5267
+ elif isinstance(item, dict):
5268
+ field_id = _coerce_count(item.get("field_id", item.get("fieldId")))
5269
+ if field_id is None or field_id < 0:
5270
+ raise_tool_error(
5271
+ QingflowApiError.config_error(
5272
+ "columns must be a list of field_id integers or {field_id} objects"
5273
+ )
5274
+ )
5275
+ normalized.append(field_id)
5276
+ return normalized
5277
+
5278
+
5279
+ def _column_selector_payload(field_id: int) -> JSONObject:
5280
+ return {"field_id": field_id}
5281
+
5282
+
5265
5283
  def _resolve_sort_ascend(item: JSONObject) -> bool:
5266
5284
  if "isAscend" in item:
5267
5285
  return bool(item["isAscend"])