@josephyan/qingflow-app-builder-mcp 0.2.0-beta.25 → 0.2.0-beta.27
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/src/qingflow_mcp/__init__.py +1 -1
- package/src/qingflow_mcp/server.py +82 -17
- package/src/qingflow_mcp/server_app_user.py +77 -9
- package/src/qingflow_mcp/tools/directory_tools.py +46 -4
- package/src/qingflow_mcp/tools/record_tools.py +892 -21
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.27
|
|
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.27 qingflow-app-builder-mcp
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
Environment:
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
from datetime import date
|
|
4
|
+
|
|
3
5
|
from mcp.server.fastmcp import FastMCP
|
|
4
6
|
|
|
5
7
|
from .backend_client import BackendClient
|
|
@@ -23,25 +25,88 @@ from .tools.workspace_tools import WorkspaceTools
|
|
|
23
25
|
|
|
24
26
|
|
|
25
27
|
def build_server() -> FastMCP:
|
|
28
|
+
today = date.today()
|
|
29
|
+
current_year = today.year
|
|
26
30
|
server = FastMCP(
|
|
27
31
|
"Qingflow MCP",
|
|
28
|
-
instructions=(
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
32
|
+
instructions=f"""Use this server for Qingflow operational workflows. Current date: `{today.isoformat()}`.
|
|
33
|
+
|
|
34
|
+
## Authentication
|
|
35
|
+
|
|
36
|
+
Use `auth_login` first, then `workspace_list` and `workspace_select`.
|
|
37
|
+
All resource tools operate with the logged-in user's Qingflow permissions.
|
|
38
|
+
|
|
39
|
+
## App Discovery
|
|
40
|
+
|
|
41
|
+
If `app_key` is unknown, use `app_list` or `app_search` first.
|
|
42
|
+
|
|
43
|
+
## Schema-First Rule
|
|
44
|
+
|
|
45
|
+
Always call `record_schema_get` before `record_list`, `record_get`, `record_write`, or `record_analyze`.
|
|
46
|
+
|
|
47
|
+
- All `field_id` values must come from the schema response.
|
|
48
|
+
- Never guess field names or ids.
|
|
49
|
+
|
|
50
|
+
## Schema Scope
|
|
51
|
+
|
|
52
|
+
`record_schema_get` returns the current user's applicant-node visible fields only.
|
|
53
|
+
|
|
54
|
+
- Hidden fields are omitted.
|
|
55
|
+
- Missing fields mean the field is not visible in the current permission scope.
|
|
56
|
+
|
|
57
|
+
## Analytics Path
|
|
58
|
+
|
|
59
|
+
`record_schema_get -> record_analyze`
|
|
60
|
+
|
|
61
|
+
Use this DSL shape:
|
|
62
|
+
|
|
63
|
+
- `dimensions`: `{{field_id, alias, bucket}}`
|
|
64
|
+
- `metrics`: `{{op, field_id, alias}}`
|
|
65
|
+
- `filters`: `{{field_id, op, value}}`
|
|
66
|
+
- `sort`: `{{by, order}}`
|
|
67
|
+
|
|
68
|
+
Important key rules:
|
|
69
|
+
|
|
70
|
+
- Use `op`
|
|
71
|
+
- Do **not** use `type`
|
|
72
|
+
- Do **not** use `agg`
|
|
73
|
+
- Do **not** use `aggregation`
|
|
74
|
+
- Do **not** use `operator`
|
|
75
|
+
|
|
76
|
+
Analysis answers must include concrete numbers. When applicable, include percentages based on the returned totals.
|
|
77
|
+
|
|
78
|
+
## Record CRUD Path
|
|
79
|
+
|
|
80
|
+
`record_schema_get -> record_list / record_get / record_write`
|
|
81
|
+
|
|
82
|
+
`record_write` uses SQL-like JSON clauses:
|
|
83
|
+
|
|
84
|
+
- `insert` -> `values`
|
|
85
|
+
- `update` -> `record_id + set`
|
|
86
|
+
- `delete` -> `record_id` or `record_ids`
|
|
87
|
+
|
|
88
|
+
- Read relation targets from `record_schema_get.target_app_key` / `target_app_name` before preparing relation writes.
|
|
89
|
+
- If a member or department field id is known but candidate ids are not, use `record_member_candidates` or `record_department_candidates` before `record_write`.
|
|
90
|
+
|
|
91
|
+
## Task Center Path
|
|
92
|
+
|
|
93
|
+
`task_summary -> task_list / task_facets -> task action`
|
|
94
|
+
|
|
95
|
+
## Time Handling
|
|
96
|
+
|
|
97
|
+
Normalize relative dates before building DSL.
|
|
98
|
+
|
|
99
|
+
- If the user says `3月` without a year, use the current year: `{current_year}`
|
|
100
|
+
- Convert month-only phrases into explicit legal date ranges
|
|
101
|
+
- Never send impossible dates such as `2026-02-29`
|
|
102
|
+
|
|
103
|
+
## Environment
|
|
104
|
+
|
|
105
|
+
Default to `prod` unless the user explicitly specifies `test`.
|
|
106
|
+
|
|
107
|
+
## Constraints
|
|
108
|
+
|
|
109
|
+
Avoid builder-side app or schema changes here.""",
|
|
45
110
|
)
|
|
46
111
|
sessions = SessionStore()
|
|
47
112
|
backend = BackendClient()
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
from datetime import date
|
|
4
|
+
|
|
3
5
|
from mcp.server.fastmcp import FastMCP
|
|
4
6
|
|
|
5
7
|
from .backend_client import BackendClient
|
|
@@ -16,17 +18,83 @@ from .tools.workspace_tools import WorkspaceTools
|
|
|
16
18
|
|
|
17
19
|
|
|
18
20
|
def build_user_server() -> FastMCP:
|
|
21
|
+
today = date.today()
|
|
22
|
+
current_year = today.year
|
|
19
23
|
server = FastMCP(
|
|
20
24
|
"Qingflow App User MCP",
|
|
21
|
-
instructions=(
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
25
|
+
instructions=f"""Use this server for Qingflow operational workflows. Current date: `{today.isoformat()}`.
|
|
26
|
+
|
|
27
|
+
## App Discovery
|
|
28
|
+
|
|
29
|
+
If `app_key` is unknown, use `app_list` or `app_search` first.
|
|
30
|
+
|
|
31
|
+
## Schema-First Rule
|
|
32
|
+
|
|
33
|
+
Always call `record_schema_get` before `record_list`, `record_get`, `record_write`, or `record_analyze`.
|
|
34
|
+
|
|
35
|
+
- All `field_id` values must come from the schema response.
|
|
36
|
+
- Never guess field names or ids.
|
|
37
|
+
|
|
38
|
+
## Schema Scope
|
|
39
|
+
|
|
40
|
+
`record_schema_get` returns the current user's applicant-node visible fields only.
|
|
41
|
+
|
|
42
|
+
- Hidden fields are omitted.
|
|
43
|
+
- Missing fields mean the field is not visible in the current permission scope.
|
|
44
|
+
|
|
45
|
+
## Analytics Path
|
|
46
|
+
|
|
47
|
+
`record_schema_get -> record_analyze`
|
|
48
|
+
|
|
49
|
+
Use this DSL shape:
|
|
50
|
+
|
|
51
|
+
- `dimensions`: `{{field_id, alias, bucket}}`
|
|
52
|
+
- `metrics`: `{{op, field_id, alias}}`
|
|
53
|
+
- `filters`: `{{field_id, op, value}}`
|
|
54
|
+
- `sort`: `{{by, order}}`
|
|
55
|
+
|
|
56
|
+
Important key rules:
|
|
57
|
+
|
|
58
|
+
- Use `op`
|
|
59
|
+
- Do **not** use `type`
|
|
60
|
+
- Do **not** use `agg`
|
|
61
|
+
- Do **not** use `aggregation`
|
|
62
|
+
- Do **not** use `operator`
|
|
63
|
+
|
|
64
|
+
Analysis answers must include concrete numbers. When applicable, include percentages based on the returned totals.
|
|
65
|
+
|
|
66
|
+
## Record CRUD Path
|
|
67
|
+
|
|
68
|
+
`record_schema_get -> record_list / record_get / record_write`
|
|
69
|
+
|
|
70
|
+
`record_write` uses SQL-like JSON clauses:
|
|
71
|
+
|
|
72
|
+
- `insert` -> `values`
|
|
73
|
+
- `update` -> `record_id + set`
|
|
74
|
+
- `delete` -> `record_id` or `record_ids`
|
|
75
|
+
|
|
76
|
+
- Read relation targets from `record_schema_get.target_app_key` / `target_app_name` before preparing relation writes.
|
|
77
|
+
- If a member or department field id is known but candidate ids are not, use `record_member_candidates` or `record_department_candidates` before `record_write`.
|
|
78
|
+
|
|
79
|
+
## Task Center Path
|
|
80
|
+
|
|
81
|
+
`task_summary -> task_list / task_facets -> task action`
|
|
82
|
+
|
|
83
|
+
## Time Handling
|
|
84
|
+
|
|
85
|
+
Normalize relative dates before building DSL.
|
|
86
|
+
|
|
87
|
+
- If the user says `3月` without a year, use the current year: `{current_year}`
|
|
88
|
+
- Convert month-only phrases into explicit legal date ranges
|
|
89
|
+
- Never send impossible dates such as `2026-02-29`
|
|
90
|
+
|
|
91
|
+
## Environment
|
|
92
|
+
|
|
93
|
+
Default to `prod` unless the user explicitly specifies `test`.
|
|
94
|
+
|
|
95
|
+
## Constraints
|
|
96
|
+
|
|
97
|
+
Avoid builder-side app or schema changes here.""",
|
|
30
98
|
)
|
|
31
99
|
sessions = SessionStore()
|
|
32
100
|
backend = BackendClient()
|
|
@@ -303,15 +303,57 @@ class DirectoryTools(ToolBase):
|
|
|
303
303
|
page_num: int,
|
|
304
304
|
page_size: int,
|
|
305
305
|
) -> dict[str, Any]:
|
|
306
|
-
if
|
|
307
|
-
raise_tool_error(QingflowApiError.config_error("
|
|
306
|
+
if page_num <= 0:
|
|
307
|
+
raise_tool_error(QingflowApiError.config_error("page_num must be positive"))
|
|
308
|
+
if page_size <= 0:
|
|
309
|
+
raise_tool_error(QingflowApiError.config_error("page_size must be positive"))
|
|
310
|
+
normalized_keyword = keyword.strip()
|
|
311
|
+
|
|
312
|
+
if not normalized_keyword:
|
|
313
|
+
def runner(session_profile, context):
|
|
314
|
+
fetch_limit = max((page_num + 1) * page_size + 1, page_size + 1)
|
|
315
|
+
items, truncated, deepest_depth = self._walk_department_tree(
|
|
316
|
+
context,
|
|
317
|
+
parent_dept_id=None,
|
|
318
|
+
max_depth=20,
|
|
319
|
+
max_items=fetch_limit,
|
|
320
|
+
)
|
|
321
|
+
start = (page_num - 1) * page_size
|
|
322
|
+
page_items = items[start : start + page_size]
|
|
323
|
+
reported_total = None if truncated else len(items)
|
|
324
|
+
page_amount = None if truncated else ((len(items) + page_size - 1) // page_size if items else 0)
|
|
325
|
+
if truncated and page_items:
|
|
326
|
+
page_amount = max(page_num + 1, (start + len(page_items) + page_size - 1) // page_size)
|
|
327
|
+
return {
|
|
328
|
+
"profile": profile,
|
|
329
|
+
"ws_id": session_profile.selected_ws_id,
|
|
330
|
+
"request_route": self._request_route_payload(context),
|
|
331
|
+
"items": page_items,
|
|
332
|
+
"pagination": {
|
|
333
|
+
"page": page_num,
|
|
334
|
+
"page_size": page_size,
|
|
335
|
+
"returned_items": len(page_items),
|
|
336
|
+
"reported_total": reported_total,
|
|
337
|
+
"page_amount": page_amount,
|
|
338
|
+
"depth_scanned": deepest_depth + 1 if page_items else 0,
|
|
339
|
+
},
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
raw = self._run(profile, runner)
|
|
343
|
+
items = [item for item in raw.get("items", []) if isinstance(item, dict)]
|
|
344
|
+
return self._public_directory_response(
|
|
345
|
+
raw,
|
|
346
|
+
items=items,
|
|
347
|
+
pagination=raw.get("pagination", {}),
|
|
348
|
+
selection={"keyword": None},
|
|
349
|
+
)
|
|
308
350
|
|
|
309
351
|
def runner(session_profile, context):
|
|
310
352
|
result = self.backend.request(
|
|
311
353
|
"GET",
|
|
312
354
|
context,
|
|
313
355
|
"/contact/deptByPage",
|
|
314
|
-
params={"keyword":
|
|
356
|
+
params={"keyword": normalized_keyword, "pageNum": page_num, "pageSize": page_size},
|
|
315
357
|
)
|
|
316
358
|
return {
|
|
317
359
|
"profile": profile,
|
|
@@ -332,7 +374,7 @@ class DirectoryTools(ToolBase):
|
|
|
332
374
|
"reported_total": _coerce_int(_payload_value(raw.get("page"), "total")),
|
|
333
375
|
"page_amount": _coerce_int(_payload_value(raw.get("page"), "pageAmount")),
|
|
334
376
|
},
|
|
335
|
-
selection={"keyword":
|
|
377
|
+
selection={"keyword": normalized_keyword},
|
|
336
378
|
)
|
|
337
379
|
|
|
338
380
|
def directory_list_all_departments(
|