@qingflow-tech/qingflow-app-user-mcp 1.0.0

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 (109) hide show
  1. package/README.md +37 -0
  2. package/docs/local-agent-install.md +332 -0
  3. package/entry_point.py +13 -0
  4. package/npm/bin/qingflow-app-user-mcp.mjs +7 -0
  5. package/npm/lib/runtime.mjs +339 -0
  6. package/npm/scripts/postinstall.mjs +16 -0
  7. package/package.json +34 -0
  8. package/pyproject.toml +67 -0
  9. package/qingflow-app-user-mcp +15 -0
  10. package/skills/qingflow-app-user/SKILL.md +79 -0
  11. package/skills/qingflow-app-user/agents/openai.yaml +4 -0
  12. package/skills/qingflow-app-user/references/data-gotchas.md +29 -0
  13. package/skills/qingflow-app-user/references/environments.md +63 -0
  14. package/skills/qingflow-app-user/references/record-patterns.md +48 -0
  15. package/skills/qingflow-app-user/references/workflow-usage.md +26 -0
  16. package/skills/qingflow-record-analysis/SKILL.md +158 -0
  17. package/skills/qingflow-record-analysis/agents/openai.yaml +4 -0
  18. package/skills/qingflow-record-analysis/references/analysis-gotchas.md +145 -0
  19. package/skills/qingflow-record-analysis/references/analysis-patterns.md +125 -0
  20. package/skills/qingflow-record-analysis/references/confidence-reporting.md +92 -0
  21. package/skills/qingflow-record-analysis/references/dsl-templates.md +93 -0
  22. package/skills/qingflow-record-delete/SKILL.md +29 -0
  23. package/skills/qingflow-record-import/SKILL.md +31 -0
  24. package/skills/qingflow-record-insert/SKILL.md +58 -0
  25. package/skills/qingflow-record-update/SKILL.md +42 -0
  26. package/skills/qingflow-task-ops/SKILL.md +123 -0
  27. package/skills/qingflow-task-ops/agents/openai.yaml +4 -0
  28. package/skills/qingflow-task-ops/references/environments.md +44 -0
  29. package/skills/qingflow-task-ops/references/workflow-usage.md +27 -0
  30. package/src/qingflow_mcp/__init__.py +5 -0
  31. package/src/qingflow_mcp/__main__.py +5 -0
  32. package/src/qingflow_mcp/backend_client.py +649 -0
  33. package/src/qingflow_mcp/builder_facade/__init__.py +3 -0
  34. package/src/qingflow_mcp/builder_facade/models.py +1836 -0
  35. package/src/qingflow_mcp/builder_facade/service.py +15044 -0
  36. package/src/qingflow_mcp/cli/__init__.py +1 -0
  37. package/src/qingflow_mcp/cli/commands/__init__.py +18 -0
  38. package/src/qingflow_mcp/cli/commands/app.py +40 -0
  39. package/src/qingflow_mcp/cli/commands/auth.py +44 -0
  40. package/src/qingflow_mcp/cli/commands/builder.py +538 -0
  41. package/src/qingflow_mcp/cli/commands/chart.py +18 -0
  42. package/src/qingflow_mcp/cli/commands/common.py +62 -0
  43. package/src/qingflow_mcp/cli/commands/imports.py +96 -0
  44. package/src/qingflow_mcp/cli/commands/portal.py +25 -0
  45. package/src/qingflow_mcp/cli/commands/record.py +331 -0
  46. package/src/qingflow_mcp/cli/commands/repo.py +80 -0
  47. package/src/qingflow_mcp/cli/commands/task.py +89 -0
  48. package/src/qingflow_mcp/cli/commands/view.py +18 -0
  49. package/src/qingflow_mcp/cli/commands/workspace.py +25 -0
  50. package/src/qingflow_mcp/cli/context.py +60 -0
  51. package/src/qingflow_mcp/cli/formatters.py +334 -0
  52. package/src/qingflow_mcp/cli/json_io.py +50 -0
  53. package/src/qingflow_mcp/cli/main.py +178 -0
  54. package/src/qingflow_mcp/config.py +513 -0
  55. package/src/qingflow_mcp/errors.py +66 -0
  56. package/src/qingflow_mcp/import_store.py +121 -0
  57. package/src/qingflow_mcp/json_types.py +18 -0
  58. package/src/qingflow_mcp/list_type_labels.py +76 -0
  59. package/src/qingflow_mcp/public_surface.py +233 -0
  60. package/src/qingflow_mcp/repository_store.py +71 -0
  61. package/src/qingflow_mcp/response_trim.py +470 -0
  62. package/src/qingflow_mcp/server.py +212 -0
  63. package/src/qingflow_mcp/server_app_builder.py +533 -0
  64. package/src/qingflow_mcp/server_app_user.py +362 -0
  65. package/src/qingflow_mcp/session_store.py +302 -0
  66. package/src/qingflow_mcp/solution/__init__.py +6 -0
  67. package/src/qingflow_mcp/solution/build_assembly_store.py +181 -0
  68. package/src/qingflow_mcp/solution/compiler/__init__.py +282 -0
  69. package/src/qingflow_mcp/solution/compiler/chart_compiler.py +96 -0
  70. package/src/qingflow_mcp/solution/compiler/form_compiler.py +495 -0
  71. package/src/qingflow_mcp/solution/compiler/icon_utils.py +187 -0
  72. package/src/qingflow_mcp/solution/compiler/navigation_compiler.py +57 -0
  73. package/src/qingflow_mcp/solution/compiler/package_compiler.py +19 -0
  74. package/src/qingflow_mcp/solution/compiler/portal_compiler.py +60 -0
  75. package/src/qingflow_mcp/solution/compiler/view_compiler.py +51 -0
  76. package/src/qingflow_mcp/solution/compiler/workflow_compiler.py +173 -0
  77. package/src/qingflow_mcp/solution/design_session.py +222 -0
  78. package/src/qingflow_mcp/solution/design_store.py +100 -0
  79. package/src/qingflow_mcp/solution/executor.py +2398 -0
  80. package/src/qingflow_mcp/solution/normalizer.py +23 -0
  81. package/src/qingflow_mcp/solution/requirements_builder.py +536 -0
  82. package/src/qingflow_mcp/solution/run_store.py +244 -0
  83. package/src/qingflow_mcp/solution/spec_models.py +855 -0
  84. package/src/qingflow_mcp/tools/__init__.py +1 -0
  85. package/src/qingflow_mcp/tools/ai_builder_tools.py +3419 -0
  86. package/src/qingflow_mcp/tools/app_tools.py +925 -0
  87. package/src/qingflow_mcp/tools/approval_tools.py +1062 -0
  88. package/src/qingflow_mcp/tools/auth_tools.py +875 -0
  89. package/src/qingflow_mcp/tools/base.py +388 -0
  90. package/src/qingflow_mcp/tools/code_block_tools.py +777 -0
  91. package/src/qingflow_mcp/tools/custom_button_tools.py +202 -0
  92. package/src/qingflow_mcp/tools/directory_tools.py +675 -0
  93. package/src/qingflow_mcp/tools/feedback_tools.py +238 -0
  94. package/src/qingflow_mcp/tools/file_tools.py +409 -0
  95. package/src/qingflow_mcp/tools/import_tools.py +2189 -0
  96. package/src/qingflow_mcp/tools/navigation_tools.py +210 -0
  97. package/src/qingflow_mcp/tools/package_tools.py +326 -0
  98. package/src/qingflow_mcp/tools/portal_tools.py +158 -0
  99. package/src/qingflow_mcp/tools/qingbi_report_tools.py +374 -0
  100. package/src/qingflow_mcp/tools/record_tools.py +14037 -0
  101. package/src/qingflow_mcp/tools/repository_dev_tools.py +552 -0
  102. package/src/qingflow_mcp/tools/resource_read_tools.py +421 -0
  103. package/src/qingflow_mcp/tools/role_tools.py +112 -0
  104. package/src/qingflow_mcp/tools/solution_tools.py +4054 -0
  105. package/src/qingflow_mcp/tools/task_context_tools.py +2228 -0
  106. package/src/qingflow_mcp/tools/task_tools.py +890 -0
  107. package/src/qingflow_mcp/tools/view_tools.py +335 -0
  108. package/src/qingflow_mcp/tools/workflow_tools.py +376 -0
  109. package/src/qingflow_mcp/tools/workspace_tools.py +125 -0
@@ -0,0 +1,470 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from copy import deepcopy
5
+ from functools import wraps
6
+ from typing import Any, Callable
7
+
8
+ from .public_surface import (
9
+ BUILDER_DOMAIN,
10
+ USER_DOMAIN,
11
+ cli_trim_key_from_namespace,
12
+ server_method_map,
13
+ tool_key,
14
+ )
15
+
16
+
17
+ JSONObject = dict[str, Any]
18
+ TransformFn = Callable[[JSONObject], None]
19
+
20
+
21
+ COMMON_SUCCESS_DROP_TOP = {
22
+ "request_route",
23
+ "profile",
24
+ "ws_id",
25
+ "output_profile",
26
+ "qf_version_source",
27
+ "persisted",
28
+ "base_url",
29
+ "normalized_args",
30
+ "suggested_next_call",
31
+ "noop",
32
+ "allowed_values",
33
+ "missing_fields",
34
+ "ok",
35
+ }
36
+
37
+ COMMON_ERROR_DROP_TOP = {
38
+ "request_route",
39
+ "base_url",
40
+ "normalized_args",
41
+ "suggested_next_call",
42
+ "profile",
43
+ "ws_id",
44
+ "output_profile",
45
+ "qf_version_source",
46
+ "persisted",
47
+ "allowed_values",
48
+ "missing_fields",
49
+ "noop",
50
+ "ok",
51
+ }
52
+
53
+ SUCCESS_POLICY_BY_TOOL: dict[str, TransformFn] = {}
54
+
55
+
56
+ def trim_public_response(tool_name: str | None, payload: dict[str, Any]) -> dict[str, Any]:
57
+ if not isinstance(payload, dict):
58
+ return payload
59
+ if _looks_like_failure_payload(payload):
60
+ return _trim_returned_failure(payload)
61
+ return trim_success_response(tool_name, payload)
62
+
63
+
64
+ def trim_success_response(tool_name: str | None, payload: dict[str, Any]) -> dict[str, Any]:
65
+ if not isinstance(payload, dict):
66
+ return payload
67
+ trimmed = deepcopy(payload)
68
+ _drop_top_keys(trimmed, COMMON_SUCCESS_DROP_TOP)
69
+ transformer = SUCCESS_POLICY_BY_TOOL.get(tool_name or "")
70
+ if transformer is not None:
71
+ transformer(trimmed)
72
+ _drop_empty_optional_keys(trimmed)
73
+ return trimmed
74
+
75
+
76
+ def trim_error_response(payload: dict[str, Any]) -> dict[str, Any]:
77
+ if not isinstance(payload, dict):
78
+ return payload
79
+ trimmed = deepcopy(payload)
80
+ _drop_top_keys(trimmed, COMMON_ERROR_DROP_TOP)
81
+ _drop_deep_keys(trimmed.get("details"), {"request_route", "base_url", "normalized_args", "suggested_next_call", "transport", "response", "body", "raw"})
82
+ details = trimmed.get("details")
83
+ if isinstance(details, dict):
84
+ compact_details = _compact_scalar_dict(details)
85
+ if compact_details:
86
+ trimmed["details"] = compact_details
87
+ else:
88
+ trimmed.pop("details", None)
89
+ if trimmed.get("backend_code") is not None:
90
+ trimmed.pop("http_status", None)
91
+ _drop_empty_optional_keys(trimmed)
92
+ return trimmed
93
+
94
+
95
+ def wrap_trimmed_methods(instance: object, method_map: dict[str, str]) -> object:
96
+ for method_name, tool_name in method_map.items():
97
+ original = getattr(instance, method_name, None)
98
+ if original is None or not callable(original):
99
+ continue
100
+ setattr(instance, method_name, _wrap_callable(original, tool_name))
101
+ return instance
102
+
103
+
104
+ def resolve_cli_tool_name(args: Any) -> str | None:
105
+ return cli_trim_key_from_namespace(args)
106
+
107
+
108
+ USER_SERVER_METHOD_MAP = server_method_map(USER_DOMAIN)
109
+ BUILDER_SERVER_METHOD_MAP = server_method_map(BUILDER_DOMAIN)
110
+
111
+
112
+ def _wrap_callable(original: Callable[..., Any], tool_name: str) -> Callable[..., Any]:
113
+ @wraps(original)
114
+ def wrapped(*args: Any, **kwargs: Any) -> Any:
115
+ try:
116
+ result = original(*args, **kwargs)
117
+ except RuntimeError as exc:
118
+ payload = _parse_runtime_error_payload(exc)
119
+ if payload is None:
120
+ raise
121
+ raise RuntimeError(json.dumps(trim_error_response(payload), ensure_ascii=False)) from None
122
+ return trim_public_response(tool_name, result) if isinstance(result, dict) else result
123
+
124
+ return wrapped
125
+
126
+
127
+ def _parse_runtime_error_payload(exc: RuntimeError) -> dict[str, Any] | None:
128
+ raw = str(exc)
129
+ try:
130
+ payload = json.loads(raw)
131
+ except json.JSONDecodeError:
132
+ return None
133
+ return payload if isinstance(payload, dict) else None
134
+
135
+
136
+ def _looks_like_failure_payload(payload: dict[str, Any]) -> bool:
137
+ if payload.get("ok") is False:
138
+ return True
139
+ status = str(payload.get("status") or "").lower()
140
+ return status in {"failed", "blocked", "verification_failed"}
141
+
142
+
143
+ def _trim_returned_failure(payload: dict[str, Any]) -> dict[str, Any]:
144
+ trimmed = deepcopy(payload)
145
+ _drop_top_keys(trimmed, COMMON_ERROR_DROP_TOP)
146
+ _drop_deep_keys(trimmed.get("details"), {"request_route", "base_url", "normalized_args", "suggested_next_call", "transport", "response", "body", "raw"})
147
+ details = trimmed.get("details")
148
+ if isinstance(details, dict):
149
+ compact_details = _compact_scalar_dict(details)
150
+ if compact_details:
151
+ trimmed["details"] = compact_details
152
+ else:
153
+ trimmed.pop("details", None)
154
+ if trimmed.get("backend_code") is not None:
155
+ trimmed.pop("http_status", None)
156
+ _drop_empty_optional_keys(trimmed)
157
+ return trimmed
158
+
159
+
160
+ def _drop_top_keys(payload: JSONObject, keys: set[str]) -> None:
161
+ for key in keys:
162
+ payload.pop(key, None)
163
+
164
+
165
+ def _drop_empty_optional_keys(payload: JSONObject) -> None:
166
+ for key in ("warnings", "verification", "details"):
167
+ value = payload.get(key)
168
+ if value in (None, [], {}, ""):
169
+ payload.pop(key, None)
170
+
171
+
172
+ def _compact_scalar_dict(payload: dict[str, Any]) -> dict[str, Any]:
173
+ compact: dict[str, Any] = {}
174
+ for key, value in payload.items():
175
+ if isinstance(value, (str, int, float, bool)) or value is None:
176
+ compact[key] = value
177
+ continue
178
+ if isinstance(value, dict):
179
+ nested = {
180
+ nested_key: nested_value
181
+ for nested_key, nested_value in value.items()
182
+ if isinstance(nested_value, (str, int, float, bool)) or nested_value is None
183
+ }
184
+ if nested:
185
+ compact[key] = nested
186
+ return compact
187
+
188
+
189
+ def _trim_item_list(payload: JSONObject, key: str, *, allowed: tuple[str, ...]) -> None:
190
+ items = payload.get(key)
191
+ if not isinstance(items, list):
192
+ return
193
+ payload[key] = [_pick(item, allowed) for item in items if isinstance(item, dict)]
194
+
195
+
196
+ def _trim_nested_item_list(payload: JSONObject, path: tuple[str, ...], *, allowed: tuple[str, ...]) -> None:
197
+ parent = _nested_dict(payload, path[:-1])
198
+ if not isinstance(parent, dict):
199
+ return
200
+ key = path[-1]
201
+ items = parent.get(key)
202
+ if not isinstance(items, list):
203
+ return
204
+ parent[key] = [_pick(item, allowed) for item in items if isinstance(item, dict)]
205
+
206
+
207
+ def _keep_nested_keys(payload: JSONObject, path: tuple[str, ...], *, allowed: tuple[str, ...]) -> None:
208
+ parent = _nested_dict(payload, path[:-1])
209
+ if not isinstance(parent, dict):
210
+ return
211
+ node = parent.get(path[-1])
212
+ if isinstance(node, dict):
213
+ parent[path[-1]] = _pick(node, allowed)
214
+
215
+
216
+ def _drop_nested_keys(payload: JSONObject, path: tuple[str, ...], *, keys: tuple[str, ...]) -> None:
217
+ parent = _nested_dict(payload, path[:-1])
218
+ if not isinstance(parent, dict):
219
+ return
220
+ node = parent.get(path[-1])
221
+ if not isinstance(node, dict):
222
+ return
223
+ for key in keys:
224
+ node.pop(key, None)
225
+
226
+
227
+ def _drop_deep_keys(payload: Any, keys: set[str]) -> None:
228
+ if isinstance(payload, dict):
229
+ for key in list(payload.keys()):
230
+ if key in keys:
231
+ payload.pop(key, None)
232
+ continue
233
+ _drop_deep_keys(payload.get(key), keys)
234
+ elif isinstance(payload, list):
235
+ for item in payload:
236
+ _drop_deep_keys(item, keys)
237
+
238
+
239
+ def _nested_dict(payload: JSONObject, path: tuple[str, ...]) -> JSONObject | None:
240
+ node: Any = payload
241
+ for key in path:
242
+ if not isinstance(node, dict):
243
+ return None
244
+ node = node.get(key)
245
+ return node if isinstance(node, dict) else None
246
+
247
+
248
+ def _pick(payload: JSONObject, allowed: tuple[str, ...]) -> JSONObject:
249
+ return {key: payload.get(key) for key in allowed if key in payload}
250
+
251
+
252
+ def _trim_auth_payload(payload: JSONObject) -> None:
253
+ pass
254
+
255
+
256
+ def _trim_auth_logout(payload: JSONObject) -> None:
257
+ pass
258
+
259
+
260
+ def _trim_workspace_list(payload: JSONObject) -> None:
261
+ page = payload.get("page")
262
+ if isinstance(page, dict):
263
+ _trim_item_list(page, "list", allowed=("wsId", "workspaceName", "remark"))
264
+
265
+
266
+ def _trim_app_search_like(payload: JSONObject) -> None:
267
+ payload.pop("apps", None)
268
+ _trim_item_list(payload, "items", allowed=("app_key", "app_name", "package_name"))
269
+
270
+
271
+ def _trim_app_get(payload: JSONObject) -> None:
272
+ _trim_builder_envelope(payload)
273
+ _trim_nested_item_list(
274
+ payload,
275
+ ("data", "accessible_views"),
276
+ allowed=("view_id", "name", "analysis_supported", "list_supported", "kind"),
277
+ )
278
+
279
+
280
+ def _trim_file_upload_info(payload: JSONObject) -> None:
281
+ pass
282
+
283
+
284
+ def _trim_file_upload_local(payload: JSONObject) -> None:
285
+ payload.pop("result", None)
286
+ payload.pop("upload_result", None)
287
+
288
+
289
+ def _trim_import_schema(payload: JSONObject) -> None:
290
+ pass
291
+
292
+
293
+ def _trim_record_schema(payload: JSONObject) -> None:
294
+ payload.pop("legacy_schema", None)
295
+
296
+
297
+ def _trim_record_write(payload: JSONObject) -> None:
298
+ data = payload.get("data")
299
+ if not isinstance(data, dict):
300
+ return
301
+ data.pop("normalized_payload", None)
302
+ data.pop("human_review", None)
303
+ data.pop("action", None)
304
+
305
+
306
+ def _trim_record_get(payload: JSONObject) -> None:
307
+ _keep_nested_keys(payload, ("data", "selection", "view"), allowed=("view_id", "name"))
308
+ _drop_nested_keys(payload, ("data", "selection"), keys=("columns", "workflow_node_id"))
309
+
310
+
311
+ def _trim_record_list(payload: JSONObject) -> None:
312
+ _keep_nested_keys(payload, ("data", "selection", "view"), allowed=("view_id", "name"))
313
+ _drop_nested_keys(payload, ("data", "selection"), keys=("columns",))
314
+
315
+
316
+ def _trim_record_analyze(payload: JSONObject) -> None:
317
+ _drop_deep_keys(payload, {"debug"})
318
+
319
+
320
+ def _trim_code_block_schema(payload: JSONObject) -> None:
321
+ payload.pop("legacy_schema", None)
322
+ _trim_nested_item_list(payload, ("code_block_fields",), allowed=("title", "selector", "bound_output_fields", "configured_aliases"))
323
+
324
+
325
+ def _trim_code_block_run(payload: JSONObject) -> None:
326
+ _drop_deep_keys(payload, {"debug", "request_route"})
327
+
328
+
329
+ def _trim_task_list(payload: JSONObject) -> None:
330
+ _drop_nested_keys(payload, ("data", "selection"), keys=("query",))
331
+
332
+
333
+ def _trim_task_get(payload: JSONObject) -> None:
334
+ _drop_deep_keys(payload, {"request_route", "output_profile"})
335
+
336
+
337
+ def _trim_directory(payload: JSONObject) -> None:
338
+ pass
339
+
340
+
341
+ def _trim_feedback(payload: JSONObject) -> None:
342
+ payload.pop("normalized_payload", None)
343
+
344
+
345
+ def _trim_builder_envelope(payload: JSONObject) -> None:
346
+ if str(payload.get("status") or "").lower() == "success":
347
+ details = payload.get("details")
348
+ if isinstance(details, dict):
349
+ _drop_deep_keys(details, {"request_route", "base_url", "normalized_args", "suggested_next_call", "transport", "response", "body", "raw"})
350
+ compact = _compact_scalar_dict(details)
351
+ if compact:
352
+ payload["details"] = compact
353
+ else:
354
+ payload.pop("details", None)
355
+
356
+
357
+ def _trim_builder_list_like(payload: JSONObject) -> None:
358
+ _trim_builder_envelope(payload)
359
+
360
+
361
+ def _register_policy(domains: tuple[str, ...], names: tuple[str, ...], transform: TransformFn) -> None:
362
+ for domain in domains:
363
+ for name in names:
364
+ SUCCESS_POLICY_BY_TOOL[tool_key(domain, name)] = transform
365
+
366
+
367
+ _register_policy((USER_DOMAIN, BUILDER_DOMAIN), ("auth_use_credential", "auth_whoami"), _trim_auth_payload)
368
+ _register_policy((USER_DOMAIN, BUILDER_DOMAIN), ("auth_logout",), _trim_auth_logout)
369
+ _register_policy((USER_DOMAIN, BUILDER_DOMAIN), ("workspace_list",), _trim_workspace_list)
370
+ _register_policy((USER_DOMAIN,), ("app_list", "app_search"), _trim_app_search_like)
371
+ _register_policy((USER_DOMAIN, BUILDER_DOMAIN), ("app_get",), _trim_app_get)
372
+ _register_policy((BUILDER_DOMAIN,), ("app_repair_code_blocks",), _trim_builder_list_like)
373
+ _register_policy((USER_DOMAIN, BUILDER_DOMAIN), ("portal_list", "portal_get", "view_get", "chart_get"), _trim_builder_list_like)
374
+ _register_policy((USER_DOMAIN,), ("file_get_upload_info",), _trim_file_upload_info)
375
+ _register_policy((USER_DOMAIN, BUILDER_DOMAIN), ("file_upload_local",), _trim_file_upload_local)
376
+ _register_policy(
377
+ (USER_DOMAIN,),
378
+ (
379
+ "record_import_schema_get",
380
+ "record_import_template_get",
381
+ "record_import_verify",
382
+ "record_import_repair_local",
383
+ "record_import_start",
384
+ "record_import_status_get",
385
+ ),
386
+ _trim_import_schema,
387
+ )
388
+ _register_policy(
389
+ (USER_DOMAIN,),
390
+ (
391
+ "record_schema_get",
392
+ "record_insert_schema_get",
393
+ "record_browse_schema_get",
394
+ "record_update_schema_get",
395
+ "record_code_block_schema_get",
396
+ ),
397
+ _trim_record_schema,
398
+ )
399
+ _register_policy((USER_DOMAIN,), ("record_insert", "record_update"), _trim_record_write)
400
+ _register_policy((USER_DOMAIN,), ("record_get",), _trim_record_get)
401
+ _register_policy((USER_DOMAIN,), ("record_list",), _trim_record_list)
402
+ _register_policy((USER_DOMAIN,), ("record_analyze",), _trim_record_analyze)
403
+ _register_policy((USER_DOMAIN,), ("record_code_block_run",), _trim_code_block_run)
404
+ _register_policy((USER_DOMAIN,), ("task_list",), _trim_task_list)
405
+ _register_policy(
406
+ (USER_DOMAIN,),
407
+ (
408
+ "task_get",
409
+ "task_action_execute",
410
+ "task_associated_report_detail_get",
411
+ "task_workflow_log_get",
412
+ ),
413
+ _trim_task_get,
414
+ )
415
+ _register_policy(
416
+ (USER_DOMAIN,),
417
+ (
418
+ "directory_search",
419
+ "directory_list_internal_users",
420
+ "directory_list_all_internal_users",
421
+ "directory_list_internal_departments",
422
+ "directory_list_all_departments",
423
+ "directory_list_sub_departments",
424
+ "directory_list_external_members",
425
+ ),
426
+ _trim_directory,
427
+ )
428
+ _register_policy((USER_DOMAIN, BUILDER_DOMAIN), ("feedback_submit",), _trim_feedback)
429
+ _register_policy(
430
+ (USER_DOMAIN,),
431
+ (
432
+ "record_member_candidates",
433
+ "record_department_candidates",
434
+ "record_delete",
435
+ ),
436
+ _trim_builder_list_like,
437
+ )
438
+ _register_policy(
439
+ (BUILDER_DOMAIN,),
440
+ (
441
+ "builder_tool_contract",
442
+ "package_get",
443
+ "package_apply",
444
+ "solution_install",
445
+ "member_search",
446
+ "role_search",
447
+ "role_create",
448
+ "app_release_edit_lock_if_mine",
449
+ "app_resolve",
450
+ "app_custom_button_list",
451
+ "app_custom_button_get",
452
+ "app_custom_button_create",
453
+ "app_custom_button_update",
454
+ "app_custom_button_delete",
455
+ "app_get_fields",
456
+ "app_repair_code_blocks",
457
+ "app_get_layout",
458
+ "app_get_views",
459
+ "app_get_flow",
460
+ "app_get_charts",
461
+ "app_schema_apply",
462
+ "app_layout_apply",
463
+ "app_flow_apply",
464
+ "app_views_apply",
465
+ "app_charts_apply",
466
+ "portal_apply",
467
+ "app_publish_verify",
468
+ ),
469
+ _trim_builder_list_like,
470
+ )
@@ -0,0 +1,212 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import date
4
+
5
+ from mcp.server.fastmcp import FastMCP
6
+
7
+ from .backend_client import BackendClient
8
+ from .session_store import SessionStore
9
+ from .tools.app_tools import AppTools
10
+ from .tools.auth_tools import AuthTools
11
+ from .tools.code_block_tools import CodeBlockTools
12
+ from .tools.feedback_tools import FeedbackTools
13
+ from .tools.file_tools import FileTools
14
+ from .tools.import_tools import ImportTools
15
+ from .tools.package_tools import PackageTools
16
+ from .tools.navigation_tools import NavigationTools
17
+ from .tools.directory_tools import DirectoryTools
18
+ from .tools.portal_tools import PortalTools
19
+ from .tools.qingbi_report_tools import QingbiReportTools
20
+ from .tools.role_tools import RoleTools
21
+ from .tools.solution_tools import SolutionTools
22
+ from .tools.task_context_tools import TaskContextTools
23
+ from .tools.view_tools import ViewTools
24
+ from .tools.workflow_tools import WorkflowTools
25
+ from .tools.workspace_tools import WorkspaceTools
26
+
27
+
28
+ def build_server() -> FastMCP:
29
+ today = date.today()
30
+ current_year = today.year
31
+ server = FastMCP(
32
+ "Qingflow MCP",
33
+ instructions=f"""Use this server for Qingflow operational workflows. Current date: `{today.isoformat()}`.
34
+
35
+ ## Authentication
36
+
37
+ Use `auth_use_credential` first when a local host such as createClaw can provide a credential. Treat the returned `wsId` and `qfVersion` as authoritative for the local session.
38
+ All resource tools operate with the logged-in user's Qingflow permissions.
39
+
40
+ ## Shared Helper
41
+
42
+ `feedback_submit` is always available as a cross-cutting helper.
43
+
44
+ - Use it when the current MCP capability is unsupported, awkward, or still cannot satisfy the user's need after reasonable use.
45
+ - It does not require Qingflow login or workspace selection.
46
+ - Call it only after the user explicitly confirms submission.
47
+
48
+ ## App Discovery
49
+
50
+ If `app_key` is unknown, use `app_list` or `app_search` first.
51
+ If the app is known but the data range is not, use `app_get` first and choose from `accessible_views`.
52
+ 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.
53
+
54
+ ## Schema-First Rule
55
+
56
+ Call `record_insert_schema_get` before `record_insert`.
57
+ Call `record_update_schema_get` before `record_update`.
58
+ Call `record_code_block_schema_get` before `record_code_block_run`.
59
+ 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`.
60
+ Call `record_import_schema_get` when the import field mapping is unclear before template download or verify.
61
+
62
+ - All `field_id` values must come from the schema response.
63
+ - Never guess field names or ids.
64
+
65
+ ## Schema Scope
66
+
67
+ `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`.
68
+ `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`.
69
+ `record_browse_schema_get(view_id=...)` returns browse-schema fields for the selected accessible view.
70
+ `record_code_block_schema_get` returns code-block-ready schema for exact code block field selection.
71
+ `record_import_schema_get` returns import-ready column metadata.
72
+
73
+ - Hidden fields are omitted.
74
+ - Missing fields mean the field is not visible in the current permission scope.
75
+ - Read the top-level schema payload directly; do not guess missing writable fields.
76
+
77
+ ## Analytics Path
78
+
79
+ `app_get -> record_browse_schema_get(view_id=...) -> record_analyze`
80
+
81
+ Prefer `view_id` entries from `accessible_views` where `analysis_supported=true`.
82
+
83
+ Use this DSL shape:
84
+
85
+ - `dimensions`: `{{field_id, alias, bucket}}`
86
+ - `metrics`: `{{op, field_id, alias}}`
87
+ - `filters`: `{{field_id, op, value}}`
88
+ - `sort`: `{{by, order}}`
89
+
90
+ Important key rules:
91
+
92
+ - Use `op`
93
+ - Do **not** use `type`
94
+ - Do **not** use `agg`
95
+ - Do **not** use `aggregation`
96
+ - Do **not** use `operator`
97
+
98
+ Analysis answers must include concrete numbers. When applicable, include percentages based on the returned totals.
99
+
100
+ ## Record CRUD Path
101
+
102
+ `app_get -> record_browse_schema_get(view_id=...) -> record_list / record_get`
103
+ `record_insert_schema_get -> record_insert`
104
+ `record_update_schema_get -> record_update`
105
+ `record_list / record_get -> record_delete`
106
+ `record_code_block_schema_get -> record_code_block_run`
107
+
108
+ - Use `columns` as `[{{field_id}}]`
109
+ - Use `where` items as `{{field_id, op, value}}`
110
+ - Use `order_by` items as `{{field_id, direction}}`
111
+ - 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
112
+
113
+ - `record_insert` uses an applicant-node `fields` map keyed by field title.
114
+ - `record_update` uses a field-title keyed `fields` map and internally selects the first accessible view that can execute the current payload.
115
+ - For insert, `runtime_linked_required_fields` means required-but-not-directly-writable fields that are usually supplied by runtime linkage or upstream context.
116
+ - 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.
117
+ - 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.
118
+ - `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.
119
+ - `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.
120
+ - `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.
121
+ - `record_delete` deletes by `record_id` or `record_ids`.
122
+ - When readback shape matters after insert or update, prefer `record_get(..., output_profile="normalized")` or `record_list(..., output_profile="normalized")`.
123
+
124
+ - Read relation targets from `record_insert_schema_get` / `record_update_schema_get` relation metadata before preparing relation writes.
125
+ - 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.
126
+ - If explicit candidate browsing is needed for default-all member or department fields, prefer those field candidate tools instead of starting with `directory_*`.
127
+
128
+ ## Code Block Path
129
+
130
+ Use `record_code_block_run` when the user wants to execute a form code-block field against an existing record.
131
+
132
+ - Always resolve the exact code-block field from `record_code_block_schema_get` first.
133
+ - Treat code-block execution as write-capable, not read-only.
134
+ - If the code block is bound to relation outputs, Qingflow may calculate target answers and write them back automatically.
135
+ - For safe debugging, pass `apply_writeback=false` and inspect the parsed alias results plus `relation.calculated_answers_preview` before allowing any writeback.
136
+ - In workflow context, pass `role=3` and the exact `workflow_node_id`.
137
+ - After execution, inspect `outputs.configured_aliases`, `outputs.alias_results`, `outputs.alias_map`, `relation.target_fields`, and `writeback.verification` before claiming success.
138
+
139
+ ## Import Path
140
+
141
+ `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`
142
+
143
+ - Check `app_get.data.import_capability` before doing import work.
144
+ - If `import_capability.can_import=false`, stop before template download, file repair, or import start.
145
+ - Import must go through `verify -> start`; do not start directly from a raw file path.
146
+ - `record_import_start` requires an explicit `being_enter_auditing` choice. Do not assume a default.
147
+ - Do not modify user-uploaded files unless the user explicitly authorizes repair.
148
+ - If repair is authorized, keep the original file and repair a copy, then run `record_import_verify` again before `record_import_start`.
149
+
150
+ ## Task Workflow Path
151
+
152
+ `task_list -> task_get -> task_action_execute`
153
+
154
+ - Use `task_associated_report_detail_get` for associated view or report details.
155
+ - Use `task_workflow_log_get` for full workflow log history.
156
+ - Task actions operate on `app_key + record_id + workflow_node_id`, not `task_id`.
157
+
158
+ ## Time Handling
159
+
160
+ Normalize relative dates before building DSL.
161
+
162
+ - If the user says `3月` without a year, use the current year: `{current_year}`
163
+ - Convert month-only phrases into explicit legal date ranges
164
+ - Never send impossible dates such as `2026-02-29`
165
+
166
+ ## Environment
167
+
168
+ Default to `prod` unless the user explicitly specifies `test`.
169
+
170
+ ## Constraints
171
+
172
+ Avoid builder-side app or schema changes here.
173
+
174
+ ## Feedback Path
175
+
176
+ 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.
177
+
178
+ - First summarize what is still not working
179
+ - Ask the user whether to submit feedback
180
+ - Call `feedback_submit` only after explicit user confirmation""",
181
+ )
182
+ sessions = SessionStore()
183
+ backend = BackendClient()
184
+ AuthTools(sessions, backend).register(server)
185
+ FeedbackTools(backend, mcp_side="通用").register(server)
186
+ WorkspaceTools(sessions, backend).register(server)
187
+ FileTools(sessions, backend).register(server)
188
+ ImportTools(sessions, backend).register(server)
189
+ CodeBlockTools(sessions, backend).register(server)
190
+ TaskContextTools(sessions, backend).register(server)
191
+ RoleTools(sessions, backend).register(server)
192
+ AppTools(sessions, backend).register(server)
193
+ QingbiReportTools(sessions, backend).register(server)
194
+ PackageTools(sessions, backend).register(server)
195
+ NavigationTools(sessions, backend).register(server)
196
+ PortalTools(sessions, backend).register(server)
197
+ DirectoryTools(sessions, backend).register(server)
198
+ WorkflowTools(sessions, backend).register(server)
199
+ ViewTools(sessions, backend).register(server)
200
+ SolutionTools(sessions, backend).register(server)
201
+ return server
202
+
203
+
204
+ mcp = build_server()
205
+
206
+
207
+ def main() -> None:
208
+ mcp.run()
209
+
210
+
211
+ if __name__ == "__main__":
212
+ main()