@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 +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-crud/SKILL.md +9 -1
- package/skills/qingflow-record-crud/references/record-patterns.md +19 -1
- package/src/qingflow_mcp/__init__.py +1 -1
- package/src/qingflow_mcp/server.py +3 -0
- package/src/qingflow_mcp/server_app_user.py +3 -0
- package/src/qingflow_mcp/tools/record_tools.py +48 -30
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.
|
|
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.
|
|
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
package/pyproject.toml
CHANGED
|
@@ -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`.
|
|
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
|
|
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`:
|
|
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
|
+
```
|
|
@@ -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
|
-
"
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
"
|
|
396
|
-
|
|
397
|
-
|
|
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["
|
|
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
|
-
|
|
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(
|
|
644
|
-
select_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":
|
|
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
|
-
|
|
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
|
|
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(
|
|
724
|
-
select_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":
|
|
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"])
|