@josephyan/qingflow-app-builder-mcp 0.2.0-beta.7 → 0.2.0-beta.71

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.
Files changed (70) hide show
  1. package/README.md +5 -3
  2. package/docs/local-agent-install.md +21 -5
  3. package/npm/bin/qingflow-app-builder-mcp.mjs +1 -1
  4. package/npm/lib/runtime.mjs +168 -12
  5. package/package.json +1 -1
  6. package/pyproject.toml +4 -1
  7. package/skills/qingflow-app-builder/SKILL.md +155 -22
  8. package/skills/qingflow-app-builder/references/create-app.md +51 -21
  9. package/skills/qingflow-app-builder/references/environments.md +1 -1
  10. package/skills/qingflow-app-builder/references/flow-actors-and-permissions.md +123 -0
  11. package/skills/qingflow-app-builder/references/gotchas.md +28 -1
  12. package/skills/qingflow-app-builder/references/solution-playbooks.md +14 -12
  13. package/skills/qingflow-app-builder/references/tool-selection.md +47 -19
  14. package/skills/qingflow-app-builder/references/update-flow.md +112 -25
  15. package/skills/qingflow-app-builder/references/update-layout.md +11 -24
  16. package/skills/qingflow-app-builder/references/update-schema.md +1 -23
  17. package/skills/qingflow-app-builder/references/update-views.md +87 -21
  18. package/skills/qingflow-app-builder-code-integrations/SKILL.md +137 -0
  19. package/skills/qingflow-app-builder-code-integrations/agents/openai.yaml +4 -0
  20. package/skills/qingflow-app-builder-code-integrations/references/code-block.md +66 -0
  21. package/skills/qingflow-app-builder-code-integrations/references/q-linker.md +77 -0
  22. package/src/qingflow_mcp/__init__.py +1 -1
  23. package/src/qingflow_mcp/backend_client.py +210 -0
  24. package/src/qingflow_mcp/builder_facade/models.py +1252 -3
  25. package/src/qingflow_mcp/builder_facade/service.py +11367 -2389
  26. package/src/qingflow_mcp/cli/__init__.py +1 -0
  27. package/src/qingflow_mcp/cli/commands/__init__.py +15 -0
  28. package/src/qingflow_mcp/cli/commands/app.py +40 -0
  29. package/src/qingflow_mcp/cli/commands/auth.py +78 -0
  30. package/src/qingflow_mcp/cli/commands/builder.py +515 -0
  31. package/src/qingflow_mcp/cli/commands/common.py +62 -0
  32. package/src/qingflow_mcp/cli/commands/imports.py +96 -0
  33. package/src/qingflow_mcp/cli/commands/record.py +304 -0
  34. package/src/qingflow_mcp/cli/commands/task.py +89 -0
  35. package/src/qingflow_mcp/cli/commands/workspace.py +33 -0
  36. package/src/qingflow_mcp/cli/context.py +48 -0
  37. package/src/qingflow_mcp/cli/formatters.py +355 -0
  38. package/src/qingflow_mcp/cli/json_io.py +50 -0
  39. package/src/qingflow_mcp/cli/main.py +149 -0
  40. package/src/qingflow_mcp/config.py +39 -0
  41. package/src/qingflow_mcp/import_store.py +121 -0
  42. package/src/qingflow_mcp/list_type_labels.py +24 -0
  43. package/src/qingflow_mcp/response_trim.py +668 -0
  44. package/src/qingflow_mcp/server.py +160 -18
  45. package/src/qingflow_mcp/server_app_builder.py +275 -68
  46. package/src/qingflow_mcp/server_app_user.py +219 -191
  47. package/src/qingflow_mcp/session_store.py +41 -1
  48. package/src/qingflow_mcp/solution/compiler/form_compiler.py +43 -4
  49. package/src/qingflow_mcp/solution/compiler/icon_utils.py +119 -45
  50. package/src/qingflow_mcp/solution/compiler/workflow_compiler.py +41 -2
  51. package/src/qingflow_mcp/solution/executor.py +107 -11
  52. package/src/qingflow_mcp/solution/spec_models.py +2 -0
  53. package/src/qingflow_mcp/tools/ai_builder_tools.py +2032 -127
  54. package/src/qingflow_mcp/tools/app_tools.py +419 -12
  55. package/src/qingflow_mcp/tools/approval_tools.py +571 -72
  56. package/src/qingflow_mcp/tools/auth_tools.py +398 -2
  57. package/src/qingflow_mcp/tools/code_block_tools.py +756 -0
  58. package/src/qingflow_mcp/tools/custom_button_tools.py +179 -0
  59. package/src/qingflow_mcp/tools/directory_tools.py +203 -31
  60. package/src/qingflow_mcp/tools/feedback_tools.py +230 -0
  61. package/src/qingflow_mcp/tools/file_tools.py +1 -0
  62. package/src/qingflow_mcp/tools/import_tools.py +2150 -0
  63. package/src/qingflow_mcp/tools/package_tools.py +18 -4
  64. package/src/qingflow_mcp/tools/portal_tools.py +31 -0
  65. package/src/qingflow_mcp/tools/qingbi_report_tools.py +109 -7
  66. package/src/qingflow_mcp/tools/record_tools.py +9894 -1104
  67. package/src/qingflow_mcp/tools/solution_tools.py +115 -3
  68. package/src/qingflow_mcp/tools/task_context_tools.py +2040 -0
  69. package/src/qingflow_mcp/tools/task_tools.py +376 -225
  70. package/src/qingflow_mcp/tools/workspace_tools.py +163 -19
@@ -1,34 +1,186 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import json
4
+ from datetime import date
5
+
3
6
  from mcp.server.fastmcp import FastMCP
4
7
 
5
8
  from .backend_client import BackendClient
6
9
  from .config import DEFAULT_PROFILE
10
+ from .response_trim import USER_SERVER_METHOD_MAP, trim_error_response, trim_public_response, wrap_trimmed_methods
7
11
  from .session_store import SessionStore
8
- from .tools.approval_tools import ApprovalTools
12
+ from .tools.app_tools import AppTools
9
13
  from .tools.auth_tools import AuthTools
14
+ from .tools.code_block_tools import CodeBlockTools
10
15
  from .tools.directory_tools import DirectoryTools
16
+ from .tools.feedback_tools import FeedbackTools
11
17
  from .tools.file_tools import FileTools
12
- from .tools.record_tools import RecordTools
13
- from .tools.task_tools import TaskTools
18
+ from .tools.import_tools import ImportTools
19
+ from .tools.task_context_tools import TaskContextTools
14
20
  from .tools.workspace_tools import WorkspaceTools
15
21
 
16
22
 
17
23
  def build_user_server() -> FastMCP:
24
+ today = date.today()
25
+ current_year = today.year
18
26
  server = FastMCP(
19
27
  "Qingflow App User MCP",
20
- instructions=(
21
- "Use this server for Qingflow record queries, record writes, task center operations, "
22
- "directory lookups, and approval actions. Avoid builder-side app or schema changes here."
23
- ),
28
+ instructions=f"""Use this server for Qingflow operational workflows. Current date: `{today.isoformat()}`.
29
+
30
+ ## App Discovery
31
+
32
+ If `app_key` is unknown, use `app_list` or `app_search` first.
33
+ If the app is known but the data range is not, use `app_get` first and choose from `accessible_views`.
34
+ If an accessible view has `analysis_supported=false`, do not use it for `record_list` or `record_analyze`. `boardView` and `ganttView` are special UI views, not list/analyze targets.
35
+
36
+ ## Shared Helper
37
+
38
+ `feedback_submit` is always available as a cross-cutting helper.
39
+
40
+ - Use it when the current MCP capability is unsupported, awkward, or still cannot satisfy the user's need after reasonable use.
41
+ - It does not require Qingflow login or workspace selection.
42
+ - Call it only after the user explicitly confirms submission.
43
+
44
+ ## Schema-First Rule
45
+
46
+ Call `record_insert_schema_get` before `record_insert`.
47
+ Call `record_update_schema_get` before `record_update`.
48
+ Call `record_code_block_schema_get` before `record_code_block_run`.
49
+ Call `app_get` first when the data range is unclear, then use `record_browse_schema_get(view_id=...)` before `record_list`, `record_get`, or `record_analyze`.
50
+ Call `record_import_schema_get` when the import field mapping is unclear before template download or verify.
51
+
52
+ - All `field_id` values must come from the schema response.
53
+ - Never guess field names or ids.
54
+
55
+ ## Schema Scope
56
+
57
+ `record_insert_schema_get` returns the current user's insert-ready applicant schema; read `required_fields`, `optional_fields`, `runtime_linked_required_fields`, and `payload_template`.
58
+ Inside `optional_fields`, any field with `may_become_required=true` is still writable, but may become required when linked visibility or option-driven runtime rules activate.
59
+ `record_update_schema_get` returns the current record's overall update-ready writable field set across matched accessible views; read `writable_fields` and `payload_template`.
60
+ `record_browse_schema_get(view_id=...)` returns browse-schema fields for the selected accessible view.
61
+ `record_code_block_schema_get` returns code-block-ready schema for exact code block field selection.
62
+ `record_import_schema_get` returns import-ready column metadata.
63
+
64
+ - Hidden fields are omitted.
65
+ - Missing fields mean the field is not visible in the current permission scope.
66
+ - Read the top-level schema payload directly; do not guess missing writable fields.
67
+
68
+ ## Analytics Path
69
+
70
+ `app_get -> record_browse_schema_get(view_id=...) -> record_analyze`
71
+
72
+ Prefer `view_id` entries from `accessible_views` where `analysis_supported=true`.
73
+
74
+ Use this DSL shape:
75
+
76
+ - `dimensions`: `{{field_id, alias, bucket}}`
77
+ - `metrics`: `{{op, field_id, alias}}`
78
+ - `filters`: `{{field_id, op, value}}`
79
+ - `sort`: `{{by, order}}`
80
+
81
+ Important key rules:
82
+
83
+ - Use `op`
84
+ - Do **not** use `type`
85
+ - Do **not** use `agg`
86
+ - Do **not** use `aggregation`
87
+ - Do **not** use `operator`
88
+
89
+ Analysis answers must include concrete numbers. When applicable, include percentages based on the returned totals.
90
+
91
+ ## Record CRUD Path
92
+
93
+ `app_get -> record_browse_schema_get(view_id=...) -> record_list / record_get`
94
+ `record_insert_schema_get -> record_insert`
95
+ `record_update_schema_get -> record_update`
96
+ `record_list / record_get -> record_delete`
97
+ `record_code_block_schema_get -> record_code_block_run`
98
+
99
+ - Use `columns` as `[{{field_id}}]`
100
+ - Use `where` items as `{{field_id, op, value}}`
101
+ - Use `order_by` items as `{{field_id, direction}}`
102
+ - Legacy forms such as bare integer `field_id`, `fieldId`, `operator`, `values`, or `order` may still parse, but they are compatibility-only and not the canonical DSL
103
+
104
+ - `record_insert` uses an applicant-node `fields` map keyed by field title.
105
+ - `record_update` uses a field-title keyed `fields` map and internally selects the first accessible view that can execute the current payload.
106
+ - For insert, `runtime_linked_required_fields` means required-but-not-directly-writable fields that are usually supplied by runtime linkage or upstream context.
107
+ - For insert, fields marked `may_become_required=true` stay in `optional_fields`; they are still directly writable, but linked visibility or option-driven rules can make them required at runtime.
108
+ - Read field-level `linkage` whenever present on `record_insert_schema_get` or `record_update_schema_get`; it is the static hint for linked visibility, reference-driven auto fill, and formula/default auto-fill behavior.
109
+ - `linkage.sources` lists upstream field titles that influence the current field; `linkage.affects_fields` lists downstream fields that may change when the current field changes.
110
+ - `linkage.kind=logic_visibility` means linked visibility or option-driven rules are involved; `linkage.kind=reference_fill` means reference/default matching logic is involved; `linkage.kind=formula_fill` means formula/default auto-fill logic is involved.
111
+ - `record_update_schema_get` exposes the overall writable field set for the record, but not every field combination is guaranteed; `record_update` still needs one single matched accessible view that can cover the payload.
112
+ - `record_delete` deletes by `record_id` or `record_ids`.
113
+ - When readback shape matters after insert or update, prefer `record_get(..., output_profile="normalized")` or `record_list(..., output_profile="normalized")`.
114
+
115
+ - Read relation targets from `record_insert_schema_get` / `record_update_schema_get` relation metadata before preparing relation writes.
116
+ - Member and department fields may be written with natural strings directly on `record_insert` / `record_update`; only fall back to `record_member_candidates` or `record_department_candidates` when the user wants explicit candidate browsing or the write returns ambiguity that needs confirmation.
117
+ - If explicit candidate browsing is needed for default-all member or department fields, prefer those field candidate tools instead of starting with `directory_*`.
118
+
119
+ ## Code Block Path
120
+
121
+ Use `record_code_block_run` when the user wants to execute a form code-block field against an existing record.
122
+
123
+ - Always resolve the exact code-block field from `record_code_block_schema_get` first.
124
+ - Treat code-block execution as write-capable, not read-only.
125
+ - If the code block is bound to relation outputs, Qingflow may calculate target answers and write them back automatically.
126
+ - For safe debugging, pass `apply_writeback=false` and inspect the parsed alias results plus `relation.calculated_answers_preview` before allowing any writeback.
127
+ - In workflow context, pass `role=3` and the exact `workflow_node_id`.
128
+ - After execution, inspect `outputs.configured_aliases`, `outputs.alias_results`, `outputs.alias_map`, `relation.target_fields`, and `writeback.verification` before claiming success.
129
+
130
+ ## Import Path
131
+
132
+ `app_get -> record_import_schema_get -> record_import_template_get -> record_import_verify -> (optional authorized record_import_repair_local) -> record_import_start -> record_import_status_get`
133
+
134
+ - Check `app_get.data.import_capability` before doing import work.
135
+ - If `import_capability.can_import=false`, stop before template download, file repair, or import start.
136
+ - Import must go through `verify -> start`; do not start directly from a raw file path.
137
+ - `record_import_start` requires an explicit `being_enter_auditing` choice. Do not assume a default.
138
+ - Do not modify user-uploaded files unless the user explicitly authorizes repair.
139
+ - If repair is authorized, keep the original file and repair a copy, then run `record_import_verify` again before `record_import_start`.
140
+
141
+ ## Task Workflow Path
142
+
143
+ `task_list -> task_get -> task_action_execute`
144
+
145
+ - Use `task_associated_report_detail_get` for associated view or report details.
146
+ - Use `task_workflow_log_get` for full workflow log history.
147
+ - Task actions operate on `app_key + record_id + workflow_node_id`, not `task_id`.
148
+
149
+ ## Time Handling
150
+
151
+ Normalize relative dates before building DSL.
152
+
153
+ - If the user says `3月` without a year, use the current year: `{current_year}`
154
+ - Convert month-only phrases into explicit legal date ranges
155
+ - Never send impossible dates such as `2026-02-29`
156
+
157
+ ## Environment
158
+
159
+ Default to `prod` unless the user explicitly specifies `test`.
160
+
161
+ ## Constraints
162
+
163
+ Avoid builder-side app or schema changes here.
164
+
165
+ ## Feedback Path
166
+
167
+ If the current MCP capability is unsupported, the workflow is awkward, or the user's need still cannot be satisfied after reasonable use, offer to submit product feedback.
168
+
169
+ - First summarize what is still not working
170
+ - Ask the user whether to submit feedback
171
+ - Call `feedback_submit` only after explicit user confirmation""",
24
172
  )
25
173
  sessions = SessionStore()
26
174
  backend = BackendClient()
27
- auth = AuthTools(sessions, backend)
28
- workspace = WorkspaceTools(sessions, backend)
29
- files = FileTools(sessions, backend)
30
- approvals = ApprovalTools(sessions, backend)
31
- tasks = TaskTools(sessions, backend)
175
+ auth = wrap_trimmed_methods(AuthTools(sessions, backend), USER_SERVER_METHOD_MAP)
176
+ apps = wrap_trimmed_methods(AppTools(sessions, backend), USER_SERVER_METHOD_MAP)
177
+ workspace = wrap_trimmed_methods(WorkspaceTools(sessions, backend), USER_SERVER_METHOD_MAP)
178
+ file_tools = wrap_trimmed_methods(FileTools(sessions, backend), USER_SERVER_METHOD_MAP)
179
+ imports = wrap_trimmed_methods(ImportTools(sessions, backend), USER_SERVER_METHOD_MAP)
180
+ feedback = FeedbackTools(backend, mcp_side="App User MCP")
181
+ code_block_tools = wrap_trimmed_methods(CodeBlockTools(sessions, backend), USER_SERVER_METHOD_MAP)
182
+ task_context_tools = wrap_trimmed_methods(TaskContextTools(sessions, backend), USER_SERVER_METHOD_MAP)
183
+ directory_tools = wrap_trimmed_methods(DirectoryTools(sessions, backend), USER_SERVER_METHOD_MAP)
32
184
 
33
185
  @server.tool()
34
186
  def auth_login(
@@ -92,6 +244,18 @@ def build_user_server() -> FastMCP:
92
244
  def workspace_select(profile: str = DEFAULT_PROFILE, ws_id: int = 0) -> dict:
93
245
  return workspace.workspace_select(profile=profile, ws_id=ws_id)
94
246
 
247
+ @server.tool()
248
+ def app_list(profile: str = DEFAULT_PROFILE) -> dict:
249
+ return apps.app_list(profile=profile)
250
+
251
+ @server.tool()
252
+ def app_search(profile: str = DEFAULT_PROFILE, keyword: str = "", page_num: int = 1, page_size: int = 50) -> dict:
253
+ return apps.app_search(profile=profile, keyword=keyword, page_num=page_num, page_size=page_size)
254
+
255
+ @server.tool()
256
+ def app_get(profile: str = DEFAULT_PROFILE, app_key: str = "") -> dict:
257
+ return apps.app_get(profile=profile, app_key=app_key)
258
+
95
259
  @server.tool()
96
260
  def file_get_upload_info(
97
261
  profile: str = DEFAULT_PROFILE,
@@ -104,7 +268,7 @@ def build_user_server() -> FastMCP:
104
268
  path_id: int | None = None,
105
269
  file_related_url: str | None = None,
106
270
  ) -> dict:
107
- return files.file_get_upload_info(
271
+ return file_tools.file_get_upload_info(
108
272
  profile=profile,
109
273
  upload_kind=upload_kind,
110
274
  file_name=file_name,
@@ -127,7 +291,7 @@ def build_user_server() -> FastMCP:
127
291
  path_id: int | None = None,
128
292
  file_related_url: str | None = None,
129
293
  ) -> dict:
130
- return files.file_upload_local(
294
+ return file_tools.file_upload_local(
131
295
  profile=profile,
132
296
  upload_kind=upload_kind,
133
297
  file_path=file_path,
@@ -138,187 +302,51 @@ def build_user_server() -> FastMCP:
138
302
  file_related_url=file_related_url,
139
303
  )
140
304
 
141
- RecordTools(sessions, backend).register(server)
142
- DirectoryTools(sessions, backend).register(server)
143
-
144
- @server.tool()
145
- def record_comment_add(
146
- profile: str = DEFAULT_PROFILE,
147
- app_key: str = "",
148
- apply_id: int = 0,
149
- payload: dict | None = None,
150
- ) -> dict:
151
- return approvals.record_comment_add(profile=profile, app_key=app_key, apply_id=apply_id, payload=payload or {})
152
-
153
- @server.tool()
154
- def record_comment_list(
155
- profile: str = DEFAULT_PROFILE,
156
- app_key: str = "",
157
- apply_id: int = 0,
158
- page_size: int = 20,
159
- list_type: int | None = None,
160
- page_num: int | None = 1,
161
- ) -> dict:
162
- return approvals.record_comment_list(
163
- profile=profile,
164
- app_key=app_key,
165
- apply_id=apply_id,
166
- page_size=page_size,
167
- list_type=list_type,
168
- page_num=page_num,
169
- )
305
+ imports.register(server)
170
306
 
171
307
  @server.tool()
172
- def record_comment_mention_candidates(
173
- profile: str = DEFAULT_PROFILE,
174
- app_key: str = "",
175
- apply_id: int = 0,
176
- page_size: int = 20,
177
- page_num: int = 1,
178
- list_type: int | None = None,
179
- keyword: str | None = None,
180
- ) -> dict:
181
- return approvals.record_comment_mention_candidates(
182
- profile=profile,
183
- app_key=app_key,
184
- apply_id=apply_id,
185
- page_size=page_size,
186
- page_num=page_num,
187
- list_type=list_type,
188
- keyword=keyword,
189
- )
190
-
191
- @server.tool()
192
- def record_comment_mark_read(profile: str = DEFAULT_PROFILE, app_key: str = "", apply_id: int = 0) -> dict:
193
- return approvals.record_comment_mark_read(profile=profile, app_key=app_key, apply_id=apply_id)
194
-
195
- @server.tool()
196
- def record_comment_stats(profile: str = DEFAULT_PROFILE, app_key: str = "", apply_id: int = 0) -> dict:
197
- return approvals.record_comment_stats(profile=profile, app_key=app_key, apply_id=apply_id)
198
-
199
- @server.tool()
200
- def task_list(
201
- profile: str = DEFAULT_PROFILE,
202
- type: int = 1,
203
- process_status: int = 1,
204
- app_key: str | None = None,
205
- node_id: int | None = None,
206
- search_key: str | None = None,
207
- page_num: int = 1,
208
- page_size: int = 20,
209
- create_time_asc: bool | None = None,
210
- ) -> dict:
211
- return tasks.task_list(
212
- profile=profile,
213
- type=type,
214
- process_status=process_status,
215
- app_key=app_key,
216
- node_id=node_id,
217
- search_key=search_key,
218
- page_num=page_num,
219
- page_size=page_size,
220
- create_time_asc=create_time_asc,
221
- )
222
-
223
- @server.tool()
224
- def task_list_grouped(
225
- profile: str = DEFAULT_PROFILE,
226
- type: int = 1,
227
- process_status: int = 1,
308
+ def feedback_submit(
309
+ category: str = "",
310
+ title: str = "",
311
+ description: str = "",
312
+ expected_behavior: str | None = None,
313
+ actual_behavior: str | None = None,
314
+ impact_scope: str | None = None,
315
+ tool_name: str | None = None,
228
316
  app_key: str | None = None,
229
- node_id: int | None = None,
230
- search_key: str | None = None,
231
- page_num: int = 1,
232
- page_size: int = 20,
233
- ) -> dict:
234
- return tasks.task_list_grouped(
235
- profile=profile,
236
- type=type,
237
- process_status=process_status,
238
- app_key=app_key,
239
- node_id=node_id,
240
- search_key=search_key,
241
- page_num=page_num,
242
- page_size=page_size,
243
- )
244
-
245
- @server.tool()
246
- def task_statistics(profile: str = DEFAULT_PROFILE, app_key: str | None = None) -> dict:
247
- return tasks.task_statistics(profile=profile, app_key=app_key)
248
-
249
- @server.tool()
250
- def task_urge(profile: str = DEFAULT_PROFILE, app_key: str = "", row_record_id: int = 0) -> dict:
251
- return tasks.task_urge(profile=profile, app_key=app_key, row_record_id=row_record_id)
252
-
253
- @server.tool(description=approvals._high_risk_tool_description(operation="approve", target="workflow task"))
254
- def task_approve(
255
- profile: str = DEFAULT_PROFILE,
256
- app_key: str = "",
257
- apply_id: int = 0,
258
- payload: dict | None = None,
259
- ) -> dict:
260
- return approvals.record_approve(profile=profile, app_key=app_key, apply_id=apply_id, payload=payload or {})
261
-
262
- @server.tool(description=approvals._high_risk_tool_description(operation="reject", target="workflow task"))
263
- def task_reject(
264
- profile: str = DEFAULT_PROFILE,
265
- app_key: str = "",
266
- apply_id: int = 0,
267
- payload: dict | None = None,
268
- ) -> dict:
269
- return approvals.record_reject(profile=profile, app_key=app_key, apply_id=apply_id, payload=payload or {})
270
-
271
- @server.tool()
272
- def task_rollback_candidates(
273
- profile: str = DEFAULT_PROFILE,
274
- app_key: str = "",
275
- apply_id: int = 0,
276
- audit_node_id: int = 0,
277
- ) -> dict:
278
- return approvals.record_rollback_candidates(
279
- profile=profile,
280
- app_key=app_key,
281
- apply_id=apply_id,
282
- audit_node_id=audit_node_id,
283
- )
284
-
285
- @server.tool()
286
- def task_rollback(
287
- profile: str = DEFAULT_PROFILE,
288
- app_key: str = "",
289
- apply_id: int = 0,
290
- payload: dict | None = None,
291
- ) -> dict:
292
- return approvals.record_rollback(profile=profile, app_key=app_key, apply_id=apply_id, payload=payload or {})
293
-
294
- @server.tool()
295
- def task_transfer_candidates(
296
- profile: str = DEFAULT_PROFILE,
297
- app_key: str = "",
298
- apply_id: int = 0,
299
- page_size: int = 20,
300
- page_num: int = 1,
301
- audit_node_id: int = 0,
302
- keyword: str | None = None,
303
- ) -> dict:
304
- return approvals.record_transfer_candidates(
305
- profile=profile,
306
- app_key=app_key,
307
- apply_id=apply_id,
308
- page_size=page_size,
309
- page_num=page_num,
310
- audit_node_id=audit_node_id,
311
- keyword=keyword,
312
- )
313
-
314
- @server.tool()
315
- def task_transfer(
316
- profile: str = DEFAULT_PROFILE,
317
- app_key: str = "",
318
- apply_id: int = 0,
319
- payload: dict | None = None,
317
+ record_id: str | int | None = None,
318
+ workflow_node_id: str | int | None = None,
319
+ note: str | None = None,
320
320
  ) -> dict:
321
- return approvals.record_transfer(profile=profile, app_key=app_key, apply_id=apply_id, payload=payload or {})
321
+ try:
322
+ return trim_public_response(
323
+ "feedback_submit",
324
+ feedback.feedback_submit(
325
+ category=category,
326
+ title=title,
327
+ description=description,
328
+ expected_behavior=expected_behavior,
329
+ actual_behavior=actual_behavior,
330
+ impact_scope=impact_scope,
331
+ tool_name=tool_name,
332
+ app_key=app_key,
333
+ record_id=record_id,
334
+ workflow_node_id=workflow_node_id,
335
+ note=note,
336
+ ),
337
+ )
338
+ except RuntimeError as exc:
339
+ try:
340
+ payload = json.loads(str(exc))
341
+ except json.JSONDecodeError:
342
+ raise
343
+ if isinstance(payload, dict):
344
+ raise RuntimeError(json.dumps(trim_error_response(payload), ensure_ascii=False)) from None
345
+ raise
346
+
347
+ code_block_tools.register(server)
348
+ task_context_tools.register(server)
349
+ directory_tools.register(server)
322
350
 
323
351
  return server
324
352
 
@@ -15,6 +15,7 @@ from .json_types import JSONObject, KeyringBackend
15
15
 
16
16
 
17
17
  KEYRING_SERVICE_NAME = "qingflow-mcp"
18
+ _UNSET = object()
18
19
 
19
20
 
20
21
  def _utcnow() -> str:
@@ -136,9 +137,13 @@ class SessionStore:
136
137
  if profile in self._logged_out_profiles:
137
138
  return None
138
139
  memory_session = self._memory_sessions.get(profile)
140
+ session_profile = self.get_profile(profile)
139
141
  if memory_session:
142
+ if session_profile is not None:
143
+ memory_session.base_url = session_profile.base_url
144
+ memory_session.qf_version = session_profile.qf_version
145
+ memory_session.qf_version_source = session_profile.qf_version_source
140
146
  return memory_session
141
- session_profile = self.get_profile(profile)
142
147
  if not session_profile or not session_profile.persisted:
143
148
  return None
144
149
  token = self._get_secret(self._token_key(profile))
@@ -179,6 +184,41 @@ class SessionStore:
179
184
  backend_session.qf_version_source = session_profile.qf_version_source
180
185
  return session_profile
181
186
 
187
+ def update_profile_metadata(
188
+ self,
189
+ profile: str,
190
+ *,
191
+ uid: int | object = _UNSET,
192
+ email: str | None | object = _UNSET,
193
+ nick_name: str | None | object = _UNSET,
194
+ selected_ws_id: int | None | object = _UNSET,
195
+ selected_ws_name: str | None | object = _UNSET,
196
+ ) -> SessionProfile:
197
+ session_profile = self.get_profile(profile)
198
+ if session_profile is None:
199
+ raise KeyError(profile)
200
+ if uid is not _UNSET:
201
+ session_profile.uid = int(uid)
202
+ if email is not _UNSET:
203
+ session_profile.email = email if isinstance(email, str) or email is None else session_profile.email
204
+ if nick_name is not _UNSET:
205
+ session_profile.nick_name = nick_name if isinstance(nick_name, str) or nick_name is None else session_profile.nick_name
206
+ if selected_ws_id is not _UNSET:
207
+ session_profile.selected_ws_id = (
208
+ int(selected_ws_id)
209
+ if isinstance(selected_ws_id, int) and not isinstance(selected_ws_id, bool)
210
+ else None
211
+ )
212
+ if selected_ws_name is not _UNSET:
213
+ session_profile.selected_ws_name = (
214
+ selected_ws_name
215
+ if isinstance(selected_ws_name, str) or selected_ws_name is None
216
+ else session_profile.selected_ws_name
217
+ )
218
+ session_profile.updated_at = _utcnow()
219
+ self._upsert_profile(session_profile)
220
+ return session_profile
221
+
182
222
  def logout(self, profile: str, forget_persisted: bool = False) -> None:
183
223
  self._memory_sessions.pop(profile, None)
184
224
  if forget_persisted:
@@ -23,6 +23,8 @@ QUESTION_TYPE_MAP = {
23
23
  FieldType.address: 21,
24
24
  FieldType.attachment: 13,
25
25
  FieldType.boolean: 10,
26
+ FieldType.q_linker: 20,
27
+ FieldType.code_block: 26,
26
28
  FieldType.relation: 25,
27
29
  FieldType.subtable: 18,
28
30
  }
@@ -243,13 +245,40 @@ def build_question(field: dict[str, Any], temp_id: int) -> tuple[dict[str, Any],
243
245
  "queDefaultType": 2,
244
246
  }
245
247
  )
248
+ if field_type == FieldType.code_block:
249
+ question.update(
250
+ {
251
+ "minOpts": -1,
252
+ "maxOpts": -1,
253
+ "codeBlockConfig": {
254
+ "configMode": int(config.get("config_mode") or 1),
255
+ "codeContent": str(config.get("code_content") or ""),
256
+ "resultAliasPath": deepcopy(config.get("result_alias_path") or []),
257
+ "beingHideOnForm": bool(config.get("being_hide_on_form", False)),
258
+ },
259
+ "autoTrigger": bool(config.get("auto_trigger", False)),
260
+ "customBtnTextStatus": bool(config.get("custom_button_text_enabled", False)),
261
+ "customBtnText": str(config.get("custom_button_text") or ""),
262
+ }
263
+ )
264
+ if field_type == FieldType.q_linker:
265
+ question.update(
266
+ {
267
+ "minOpts": -1,
268
+ "maxOpts": -1,
269
+ "remoteLookupConfig": deepcopy(config.get("remote_lookup_config") or config),
270
+ "autoTrigger": bool(config.get("auto_trigger", False)),
271
+ "customBtnTextStatus": bool(config.get("custom_button_text_enabled", False)),
272
+ "customBtnText": str(config.get("custom_button_text") or ""),
273
+ }
274
+ )
246
275
  if field_type == FieldType.subtable:
247
276
  sub_questions: list[dict[str, Any]] = []
248
277
  for subfield in field.get("subfields", []):
249
278
  sub_question, next_temp_id = build_question(subfield, next_temp_id)
250
279
  sub_questions.append(sub_question)
251
280
  question["subQuestions"] = sub_questions
252
- question["innerQuestions"] = deepcopy(sub_questions)
281
+ question["innerQuestions"] = [deepcopy(sub_questions)]
253
282
  question["queDefaultValues"] = {"queId": temp_id, "queTitle": field["label"], "queType": que_type, "values": [], "tableValues": []}
254
283
  return question, next_temp_id
255
284
 
@@ -257,13 +286,16 @@ def build_question(field: dict[str, Any], temp_id: int) -> tuple[dict[str, Any],
257
286
  def build_reference_config(field: dict[str, Any], temp_id: int) -> dict[str, Any]:
258
287
  config = field.get("config") or {}
259
288
  display_field_id = field.get("target_field_id") or "title"
289
+ display_field_que_id = field.get("target_field_que_id") or config.get("target_field_que_id") or 0
260
290
  display_field_label = config.get("target_field_label") or "__TARGET_FIELD_LABEL__"
261
291
  refer_field_ids = list(config.get("refer_field_ids") or [display_field_id])
292
+ refer_field_que_ids = list(config.get("refer_field_que_ids") or [])
262
293
  refer_field_labels = config.get("refer_field_labels") or [display_field_label]
263
294
  refer_field_types = config.get("refer_field_types")
264
295
  refer_questions = []
265
296
  for ordinal, field_id in enumerate(refer_field_ids, start=1):
266
297
  label = refer_field_labels[ordinal - 1] if ordinal - 1 < len(refer_field_labels) else field_id
298
+ que_id = refer_field_que_ids[ordinal - 1] if ordinal - 1 < len(refer_field_que_ids) else 0
267
299
  raw_type = _reference_field_type_value(
268
300
  refer_field_types,
269
301
  field_id=field_id,
@@ -272,7 +304,7 @@ def build_reference_config(field: dict[str, Any], temp_id: int) -> dict[str, Any
272
304
  )
273
305
  refer_questions.append(
274
306
  {
275
- "queId": 0,
307
+ "queId": que_id,
276
308
  "queTitle": label,
277
309
  "queType": _normalize_reference_que_type(raw_type) or "2",
278
310
  "queAuth": 1,
@@ -282,15 +314,22 @@ def build_reference_config(field: dict[str, Any], temp_id: int) -> dict[str, Any
282
314
  }
283
315
  )
284
316
  auth_field_ids = list(config.get("auth_field_ids") or refer_field_ids)
317
+ auth_field_que_ids = list(config.get("auth_field_que_ids") or [])
318
+ auth_ques = deepcopy(config.get("refer_auth_ques") or [])
319
+ if not auth_ques:
320
+ auth_ques = []
321
+ for ordinal, field_id in enumerate(auth_field_ids, start=1):
322
+ que_id = auth_field_que_ids[ordinal - 1] if ordinal - 1 < len(auth_field_que_ids) else 0
323
+ auth_ques.append({"queId": que_id, "queAuth": 1, "_field_id": field_id})
285
324
  return {
286
325
  "referAppKey": "__TARGET_APP_KEY__",
287
- "referQueId": 0,
326
+ "referQueId": display_field_que_id,
288
327
  "customButtonText": config.get("custom_button_text") or "选择数据",
289
328
  "beingTableSource": False,
290
329
  "referQuestions": refer_questions,
291
330
  "referMatchRules": deepcopy(config.get("refer_match_rules") or []),
292
331
  "referFillRules": deepcopy(config.get("refer_fill_rules") or []),
293
- "referAuthQues": deepcopy(config.get("refer_auth_ques") or [{"queId": 0, "queAuth": 1, "_field_id": field_id} for field_id in auth_field_ids]),
332
+ "referAuthQues": auth_ques,
294
333
  "canAddData": bool(config.get("can_add_data", False)),
295
334
  "dataAdditionButtonText": config.get("data_addition_button_text") or "新增数据",
296
335
  "canViewProcessLog": bool(config.get("can_view_process_log", True)),