@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 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.25
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.25 qingflow-app-builder-mcp
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@josephyan/qingflow-app-builder-mcp",
3
- "version": "0.2.0-beta.25",
3
+ "version": "0.2.0-beta.27",
4
4
  "description": "Builder MCP for Qingflow app/package/system design and staged solution 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.0b25"
7
+ version = "0.2.0b27"
8
8
  description = "User-authenticated MCP server for Qingflow"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -2,4 +2,4 @@ from __future__ import annotations
2
2
 
3
3
  __all__ = ["__version__"]
4
4
 
5
- __version__ = "0.2.0b25"
5
+ __version__ = "0.2.0b27"
@@ -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
- "Use auth_login first, then workspace_list and workspace_select. "
30
- "All resource tools operate with the logged-in user's Qingflow permissions.\n\n"
31
- "If app_key is unknown, use app_list or app_search first to discover current-user visible apps in the selected workspace. "
32
- "For analytics, use record_schema_get first, let the model build field_id-based DSL, "
33
- "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. "
34
- "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. "
35
- "For operational record reads, use record_schema_get first, then record_list or record_get. "
36
- "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"
37
- "Task Center (待办/已办) handling:\n"
38
- "- Use task_summary to get headline counts.\n"
39
- "- Use task_list for flat task browsing with task_box and flow_status.\n"
40
- "- Use task_facets when worksheet or workflow-node buckets matter.\n"
41
- "- Use task_mark_read to mark a specific task as read.\n"
42
- "- Use task_urge to send an urgent reminder for a pending task.\n"
43
- "- After identifying the exact task node and record, use task_approve, task_reject, task_rollback, or task_transfer as needed."
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
- "Use this server for Qingflow operational workflows with a schema-first path. "
23
- "If app_key is unknown, use app_list or app_search first to discover current-user visible apps in the selected workspace. "
24
- "For records, start with record_schema_get, then choose record_list, record_get, or record_write. "
25
- "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. "
26
- "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. "
27
- "For task center, use task_summary, task_list, and task_facets before any explicit task action. "
28
- "Avoid builder-side app or schema changes here."
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 not keyword:
307
- raise_tool_error(QingflowApiError.config_error("keyword is required"))
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": keyword, "pageNum": page_num, "pageSize": page_size},
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": keyword},
377
+ selection={"keyword": normalized_keyword},
336
378
  )
337
379
 
338
380
  def directory_list_all_departments(