@josephyan/qingflow-app-builder-mcp 0.2.0-beta.22 → 0.2.0-beta.23
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-builder-mcp@0.2.0-beta.
|
|
6
|
+
npm install @josephyan/qingflow-app-builder-mcp@0.2.0-beta.23
|
|
7
7
|
```
|
|
8
8
|
|
|
9
9
|
Run:
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
|
-
npx -y -p @josephyan/qingflow-app-builder-mcp@0.2.0-beta.
|
|
12
|
+
npx -y -p @josephyan/qingflow-app-builder-mcp@0.2.0-beta.23 qingflow-app-builder-mcp
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
Environment:
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
|
@@ -30,8 +30,9 @@ def build_server() -> FastMCP:
|
|
|
30
30
|
"All resource tools operate with the logged-in user's Qingflow permissions.\n\n"
|
|
31
31
|
"For analytics, use record_schema_get first, let the model build field_id-based DSL, "
|
|
32
32
|
"then call record_analyze. record_analyze returns compact business-first output as query/result/ranking/ratios/completeness/presentation; use verbose only for route/debug details. "
|
|
33
|
+
"record_schema_get returns the current user's applicant-node visible schema only; hidden fields are omitted and missing fields should be treated as not visible in the current permission scope. "
|
|
33
34
|
"For operational record reads, use record_schema_get first, then record_list or record_get. "
|
|
34
|
-
"For writes, use record_schema_get and then call record_write once; it performs internal preflight before any apply.\n\n"
|
|
35
|
+
"For writes, use record_schema_get and then call record_write once; it performs internal preflight before any apply and refuses fields outside the applicant-node writable schema.\n\n"
|
|
35
36
|
"Task Center (待办/已办) handling:\n"
|
|
36
37
|
"- Use task_summary to get headline counts.\n"
|
|
37
38
|
"- Use task_list for flat task browsing with task_box and flow_status.\n"
|
|
@@ -20,6 +20,7 @@ def build_user_server() -> FastMCP:
|
|
|
20
20
|
instructions=(
|
|
21
21
|
"Use this server for Qingflow operational workflows with a schema-first path. "
|
|
22
22
|
"For records, start with record_schema_get, then choose record_list, record_get, or record_write. "
|
|
23
|
+
"record_schema_get returns the current user's applicant-node visible schema only; hidden fields are omitted and missing fields should be treated as not visible in the current permission scope. "
|
|
23
24
|
"For analytics, switch to record_schema_get and record_analyze; its default output is compact query/result/ranking/ratios/completeness/presentation, with route/debug only in verbose mode. "
|
|
24
25
|
"For task center, use task_summary, task_list, and task_facets before any explicit task action. "
|
|
25
26
|
"Avoid builder-side app or schema changes here."
|
|
@@ -90,6 +90,14 @@ class ViewSelection:
|
|
|
90
90
|
conditions: list[list[ViewFilterCondition]]
|
|
91
91
|
|
|
92
92
|
|
|
93
|
+
@dataclass(slots=True)
|
|
94
|
+
class WorkflowNodeRef:
|
|
95
|
+
workflow_node_id: int
|
|
96
|
+
name: str
|
|
97
|
+
type: str
|
|
98
|
+
raw: JSONObject
|
|
99
|
+
|
|
100
|
+
|
|
93
101
|
@dataclass(slots=True)
|
|
94
102
|
class RecordInputError(Exception):
|
|
95
103
|
message: str
|
|
@@ -134,7 +142,8 @@ FIELD_LOOKUP_STRIP_RE = re.compile(r"[\s_()()\[\]【】{}<>·/\\::-]+")
|
|
|
134
142
|
class RecordTools(ToolBase):
|
|
135
143
|
def __init__(self, sessions, backend) -> None: # type: ignore[no-untyped-def]
|
|
136
144
|
super().__init__(sessions, backend)
|
|
137
|
-
self._form_cache: dict[tuple[str, str], JSONObject] = {}
|
|
145
|
+
self._form_cache: dict[tuple[str, str, str, int], JSONObject] = {}
|
|
146
|
+
self._applicant_node_cache: dict[tuple[str, str], WorkflowNodeRef] = {}
|
|
138
147
|
self._view_list_cache: dict[tuple[str, str], list[JSONObject]] = {}
|
|
139
148
|
self._view_config_cache: dict[tuple[str, str], JSONObject] = {}
|
|
140
149
|
|
|
@@ -286,9 +295,10 @@ class RecordTools(ToolBase):
|
|
|
286
295
|
raise_tool_error(QingflowApiError.config_error("app_key is required"))
|
|
287
296
|
|
|
288
297
|
def runner(session_profile, context):
|
|
298
|
+
applicant_node = self._resolve_applicant_node(profile, context, app_key, force_refresh=False)
|
|
289
299
|
index = self._get_field_index(profile, context, app_key, force_refresh=False)
|
|
290
300
|
view_selection = self._resolve_view_selection(profile, context, app_key, view_key=view_key, view_name=view_name)
|
|
291
|
-
fields = [self._schema_field_payload(field) for field in index.by_id.values()]
|
|
301
|
+
fields = [self._schema_field_payload(field, workflow_node_id=applicant_node.workflow_node_id) for field in index.by_id.values()]
|
|
292
302
|
suggested_dimensions = [
|
|
293
303
|
{"field_id": item["field_id"], "title": item["title"]}
|
|
294
304
|
for item in fields
|
|
@@ -312,6 +322,12 @@ class RecordTools(ToolBase):
|
|
|
312
322
|
"request_route": self._request_route_payload(context),
|
|
313
323
|
"data": {
|
|
314
324
|
"app_key": app_key,
|
|
325
|
+
"schema_scope": "applicant_node",
|
|
326
|
+
"workflow_node": {
|
|
327
|
+
"workflow_node_id": applicant_node.workflow_node_id,
|
|
328
|
+
"name": applicant_node.name,
|
|
329
|
+
"type": applicant_node.type,
|
|
330
|
+
},
|
|
315
331
|
"view_resolution": _view_selection_payload(view_selection),
|
|
316
332
|
"fields": fields,
|
|
317
333
|
"suggested_dimensions": suggested_dimensions,
|
|
@@ -531,31 +547,39 @@ class RecordTools(ToolBase):
|
|
|
531
547
|
}
|
|
532
548
|
return response
|
|
533
549
|
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
"
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
"
|
|
551
|
-
"
|
|
552
|
-
"
|
|
553
|
-
|
|
554
|
-
"
|
|
555
|
-
"
|
|
550
|
+
def runner(session_profile, context):
|
|
551
|
+
index = self._get_field_index(profile, context, app_key, force_refresh=False)
|
|
552
|
+
selected_fields = list(index.by_id.values())
|
|
553
|
+
result = self.backend.request(
|
|
554
|
+
"GET",
|
|
555
|
+
context,
|
|
556
|
+
f"/app/{app_key}/apply/{record_id}",
|
|
557
|
+
params={"role": 1},
|
|
558
|
+
)
|
|
559
|
+
answer_list = result.get("answers") if isinstance(result, dict) and isinstance(result.get("answers"), list) else []
|
|
560
|
+
row = _build_flat_row(cast(list[JSONValue], answer_list), selected_fields, apply_id=record_id)
|
|
561
|
+
response: JSONObject = {
|
|
562
|
+
"profile": profile,
|
|
563
|
+
"ws_id": session_profile.selected_ws_id,
|
|
564
|
+
"ok": True,
|
|
565
|
+
"request_route": self._request_route_payload(context),
|
|
566
|
+
"warnings": [],
|
|
567
|
+
"output_profile": normalized_output_profile,
|
|
568
|
+
"data": {
|
|
569
|
+
"app_key": app_key,
|
|
570
|
+
"record_id": record_id,
|
|
571
|
+
"record": row,
|
|
572
|
+
"selection": {
|
|
573
|
+
"columns": columns,
|
|
574
|
+
"workflow_node_id": workflow_node_id,
|
|
575
|
+
},
|
|
556
576
|
},
|
|
557
|
-
}
|
|
558
|
-
|
|
577
|
+
}
|
|
578
|
+
if normalized_output_profile == "verbose":
|
|
579
|
+
response["data"]["debug"] = {"raw_record": result}
|
|
580
|
+
return response
|
|
581
|
+
|
|
582
|
+
return self._run_record_tool(profile, runner)
|
|
559
583
|
|
|
560
584
|
def record_write(
|
|
561
585
|
self,
|
|
@@ -712,7 +736,7 @@ class RecordTools(ToolBase):
|
|
|
712
736
|
preflight=None,
|
|
713
737
|
)
|
|
714
738
|
|
|
715
|
-
def _schema_field_payload(self, field: FormField) -> JSONObject:
|
|
739
|
+
def _schema_field_payload(self, field: FormField, *, workflow_node_id: int) -> JSONObject:
|
|
716
740
|
write_hints = self._schema_write_hints(field)
|
|
717
741
|
return {
|
|
718
742
|
"field_id": field.que_id,
|
|
@@ -725,6 +749,8 @@ class RecordTools(ToolBase):
|
|
|
725
749
|
"role_hints": self._schema_role_hints(field),
|
|
726
750
|
"readable": True,
|
|
727
751
|
"writable": write_hints["writable"],
|
|
752
|
+
"permission_scope": "applicant_node",
|
|
753
|
+
"workflow_node_id": workflow_node_id,
|
|
728
754
|
"write_kind": write_hints["write_kind"],
|
|
729
755
|
"supported_read_ops": write_hints["supported_read_ops"],
|
|
730
756
|
"supported_write_ops": write_hints["supported_write_ops"],
|
|
@@ -2390,10 +2416,16 @@ class RecordTools(ToolBase):
|
|
|
2390
2416
|
return self._run_record_tool(profile, runner)
|
|
2391
2417
|
|
|
2392
2418
|
def _get_form_schema(self, profile: str, context, app_key: str, *, force_refresh: bool) -> JSONObject: # type: ignore[no-untyped-def]
|
|
2393
|
-
|
|
2419
|
+
applicant_node = self._resolve_applicant_node(profile, context, app_key, force_refresh=force_refresh)
|
|
2420
|
+
cache_key = (profile, app_key, "applicant_node", applicant_node.workflow_node_id)
|
|
2394
2421
|
if not force_refresh and cache_key in self._form_cache:
|
|
2395
2422
|
return self._form_cache[cache_key]
|
|
2396
|
-
schema = self.backend.request(
|
|
2423
|
+
schema = self.backend.request(
|
|
2424
|
+
"GET",
|
|
2425
|
+
context,
|
|
2426
|
+
f"/app/{app_key}/form",
|
|
2427
|
+
params={"type": 1, "beingApply": True, "auditNodeId": applicant_node.workflow_node_id},
|
|
2428
|
+
)
|
|
2397
2429
|
normalized = _normalize_form_schema(schema)
|
|
2398
2430
|
self._form_cache[cache_key] = normalized
|
|
2399
2431
|
return normalized
|
|
@@ -2401,6 +2433,26 @@ class RecordTools(ToolBase):
|
|
|
2401
2433
|
def _get_field_index(self, profile: str, context, app_key: str, *, force_refresh: bool) -> FieldIndex: # type: ignore[no-untyped-def]
|
|
2402
2434
|
return _build_field_index(self._get_form_schema(profile, context, app_key, force_refresh=force_refresh))
|
|
2403
2435
|
|
|
2436
|
+
def _resolve_applicant_node(self, profile: str, context, app_key: str, *, force_refresh: bool) -> WorkflowNodeRef: # type: ignore[no-untyped-def]
|
|
2437
|
+
cache_key = (profile, app_key)
|
|
2438
|
+
if not force_refresh and cache_key in self._applicant_node_cache:
|
|
2439
|
+
return self._applicant_node_cache[cache_key]
|
|
2440
|
+
payload = self.backend.request("GET", context, f"/app/{app_key}/auditNodes")
|
|
2441
|
+
applicant_node = _extract_applicant_node(payload)
|
|
2442
|
+
if applicant_node is None:
|
|
2443
|
+
raise_tool_error(
|
|
2444
|
+
QingflowApiError(
|
|
2445
|
+
category="config",
|
|
2446
|
+
message=f"cannot resolve applicant node for app {app_key}",
|
|
2447
|
+
details={
|
|
2448
|
+
"error_code": "APPLICANT_NODE_NOT_FOUND",
|
|
2449
|
+
"fix_hint": "Ensure the app has a workflow applicant node before using user-side record tools.",
|
|
2450
|
+
},
|
|
2451
|
+
)
|
|
2452
|
+
)
|
|
2453
|
+
self._applicant_node_cache[cache_key] = applicant_node
|
|
2454
|
+
return applicant_node
|
|
2455
|
+
|
|
2404
2456
|
def _get_view_list(self, profile: str, context, app_key: str) -> list[JSONObject]: # type: ignore[no-untyped-def]
|
|
2405
2457
|
cache_key = (profile, app_key)
|
|
2406
2458
|
if cache_key in self._view_list_cache:
|
|
@@ -3883,6 +3935,30 @@ def _normalize_view_list(payload: JSONValue) -> list[JSONObject]:
|
|
|
3883
3935
|
return flattened
|
|
3884
3936
|
|
|
3885
3937
|
|
|
3938
|
+
def _normalize_audit_nodes(payload: JSONValue) -> list[JSONObject]:
|
|
3939
|
+
if isinstance(payload, list):
|
|
3940
|
+
return [item for item in payload if isinstance(item, dict)]
|
|
3941
|
+
if isinstance(payload, dict):
|
|
3942
|
+
return [item for item in payload.values() if isinstance(item, dict)]
|
|
3943
|
+
return []
|
|
3944
|
+
|
|
3945
|
+
|
|
3946
|
+
def _extract_applicant_node(payload: JSONValue) -> WorkflowNodeRef | None:
|
|
3947
|
+
for item in _normalize_audit_nodes(payload):
|
|
3948
|
+
node_type = _coerce_count(item.get("type"))
|
|
3949
|
+
deal_type = _coerce_count(item.get("dealType"))
|
|
3950
|
+
workflow_node_id = _coerce_count(item.get("auditNodeId"))
|
|
3951
|
+
if workflow_node_id is None or node_type != 0 or deal_type != 3:
|
|
3952
|
+
continue
|
|
3953
|
+
return WorkflowNodeRef(
|
|
3954
|
+
workflow_node_id=workflow_node_id,
|
|
3955
|
+
name=_normalize_optional_text(item.get("auditNodeName")) or str(workflow_node_id),
|
|
3956
|
+
type="applicant",
|
|
3957
|
+
raw=item,
|
|
3958
|
+
)
|
|
3959
|
+
return None
|
|
3960
|
+
|
|
3961
|
+
|
|
3886
3962
|
def _compile_view_conditions(config: JSONObject) -> list[list[ViewFilterCondition]]:
|
|
3887
3963
|
raw_limit = config.get("viewgraphLimit")
|
|
3888
3964
|
if not isinstance(raw_limit, list):
|