@josephyan/qingflow-app-builder-mcp 0.1.0-beta.10

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 (55) hide show
  1. package/README.md +21 -0
  2. package/docs/local-agent-install.md +228 -0
  3. package/entry_point.py +13 -0
  4. package/npm/bin/qingflow-app-builder-mcp.mjs +7 -0
  5. package/npm/lib/runtime.mjs +146 -0
  6. package/npm/scripts/postinstall.mjs +12 -0
  7. package/package.json +33 -0
  8. package/pyproject.toml +64 -0
  9. package/qingflow-app-builder-mcp +15 -0
  10. package/src/qingflow_mcp/__init__.py +5 -0
  11. package/src/qingflow_mcp/__main__.py +5 -0
  12. package/src/qingflow_mcp/backend_client.py +336 -0
  13. package/src/qingflow_mcp/config.py +182 -0
  14. package/src/qingflow_mcp/errors.py +66 -0
  15. package/src/qingflow_mcp/json_types.py +18 -0
  16. package/src/qingflow_mcp/list_type_labels.py +52 -0
  17. package/src/qingflow_mcp/server.py +70 -0
  18. package/src/qingflow_mcp/server_app_builder.py +352 -0
  19. package/src/qingflow_mcp/server_app_user.py +334 -0
  20. package/src/qingflow_mcp/session_store.py +249 -0
  21. package/src/qingflow_mcp/solution/__init__.py +6 -0
  22. package/src/qingflow_mcp/solution/build_assembly_store.py +137 -0
  23. package/src/qingflow_mcp/solution/compiler/__init__.py +265 -0
  24. package/src/qingflow_mcp/solution/compiler/chart_compiler.py +96 -0
  25. package/src/qingflow_mcp/solution/compiler/form_compiler.py +456 -0
  26. package/src/qingflow_mcp/solution/compiler/icon_utils.py +113 -0
  27. package/src/qingflow_mcp/solution/compiler/navigation_compiler.py +57 -0
  28. package/src/qingflow_mcp/solution/compiler/package_compiler.py +19 -0
  29. package/src/qingflow_mcp/solution/compiler/portal_compiler.py +60 -0
  30. package/src/qingflow_mcp/solution/compiler/view_compiler.py +51 -0
  31. package/src/qingflow_mcp/solution/compiler/workflow_compiler.py +134 -0
  32. package/src/qingflow_mcp/solution/design_session.py +222 -0
  33. package/src/qingflow_mcp/solution/design_store.py +100 -0
  34. package/src/qingflow_mcp/solution/executor.py +2065 -0
  35. package/src/qingflow_mcp/solution/normalizer.py +23 -0
  36. package/src/qingflow_mcp/solution/run_store.py +221 -0
  37. package/src/qingflow_mcp/solution/spec_models.py +853 -0
  38. package/src/qingflow_mcp/tools/__init__.py +1 -0
  39. package/src/qingflow_mcp/tools/app_tools.py +406 -0
  40. package/src/qingflow_mcp/tools/approval_tools.py +498 -0
  41. package/src/qingflow_mcp/tools/auth_tools.py +514 -0
  42. package/src/qingflow_mcp/tools/base.py +81 -0
  43. package/src/qingflow_mcp/tools/directory_tools.py +476 -0
  44. package/src/qingflow_mcp/tools/file_tools.py +375 -0
  45. package/src/qingflow_mcp/tools/navigation_tools.py +177 -0
  46. package/src/qingflow_mcp/tools/package_tools.py +198 -0
  47. package/src/qingflow_mcp/tools/portal_tools.py +100 -0
  48. package/src/qingflow_mcp/tools/qingbi_report_tools.py +235 -0
  49. package/src/qingflow_mcp/tools/record_tools.py +4307 -0
  50. package/src/qingflow_mcp/tools/role_tools.py +94 -0
  51. package/src/qingflow_mcp/tools/solution_tools.py +2684 -0
  52. package/src/qingflow_mcp/tools/task_tools.py +692 -0
  53. package/src/qingflow_mcp/tools/view_tools.py +280 -0
  54. package/src/qingflow_mcp/tools/workflow_tools.py +238 -0
  55. package/src/qingflow_mcp/tools/workspace_tools.py +170 -0
@@ -0,0 +1 @@
1
+ from __future__ import annotations
@@ -0,0 +1,406 @@
1
+ from __future__ import annotations
2
+
3
+ from collections import Counter
4
+ from typing import Any
5
+
6
+ from mcp.server.fastmcp import FastMCP
7
+
8
+ from ..config import DEFAULT_PROFILE
9
+ from ..errors import QingflowApiError, raise_tool_error
10
+ from ..json_types import JSONObject
11
+ from ..list_type_labels import get_app_publish_status_label
12
+ from .base import ToolBase
13
+
14
+
15
+ class AppTools(ToolBase):
16
+ def register(self, mcp: FastMCP) -> None:
17
+ @mcp.tool()
18
+ def app_list(profile: str = DEFAULT_PROFILE, ship_auth: bool = False) -> JSONObject:
19
+ return self.app_list(profile=profile, ship_auth=ship_auth)
20
+
21
+ @mcp.tool()
22
+ def app_search(profile: str = DEFAULT_PROFILE, keyword: str = "", page_num: int = 1, page_size: int = 50) -> JSONObject:
23
+ return self.app_search(profile=profile, keyword=keyword, page_num=page_num, page_size=page_size)
24
+
25
+ @mcp.tool()
26
+ def app_get_base(profile: str = DEFAULT_PROFILE, app_key: str = "", include_raw: bool = False) -> JSONObject:
27
+ return self.app_get_base(profile=profile, app_key=app_key, include_raw=include_raw)
28
+
29
+ @mcp.tool(description=self._high_risk_tool_description(operation="update", target="app base settings"))
30
+ def app_update_base(profile: str = DEFAULT_PROFILE, app_key: str = "", payload: JSONObject | None = None) -> JSONObject:
31
+ return self.app_update_base(profile=profile, app_key=app_key, payload=payload or {})
32
+
33
+ @mcp.tool()
34
+ def app_get_form_schema(
35
+ profile: str = DEFAULT_PROFILE,
36
+ app_key: str = "",
37
+ form_type: int = 1,
38
+ being_draft: bool | None = None,
39
+ being_apply: bool | None = None,
40
+ audit_node_id: int | None = None,
41
+ include_raw: bool = False,
42
+ ) -> JSONObject:
43
+ return self.app_get_form_schema(
44
+ profile=profile,
45
+ app_key=app_key,
46
+ form_type=form_type,
47
+ being_draft=being_draft,
48
+ being_apply=being_apply,
49
+ audit_node_id=audit_node_id,
50
+ include_raw=include_raw,
51
+ )
52
+
53
+ @mcp.tool()
54
+ def app_get_edit_version_no(profile: str = DEFAULT_PROFILE, app_key: str = "") -> JSONObject:
55
+ return self.app_get_edit_version_no(profile=profile, app_key=app_key)
56
+
57
+ @mcp.tool()
58
+ def app_edit_finished(profile: str = DEFAULT_PROFILE, app_key: str = "", payload: JSONObject | None = None) -> JSONObject:
59
+ return self.app_edit_finished(profile=profile, app_key=app_key, payload=payload or {})
60
+
61
+ @mcp.tool()
62
+ def app_create(profile: str = DEFAULT_PROFILE, payload: JSONObject | None = None) -> JSONObject:
63
+ return self.app_create(profile=profile, payload=payload or {})
64
+
65
+ @mcp.tool(description=self._high_risk_tool_description(operation="delete", target="app configuration"))
66
+ def app_delete(profile: str = DEFAULT_PROFILE, app_key: str = "") -> JSONObject:
67
+ return self.app_delete(profile=profile, app_key=app_key)
68
+
69
+ @mcp.tool()
70
+ def app_publish(
71
+ profile: str = DEFAULT_PROFILE,
72
+ app_key: str = "",
73
+ payload: JSONObject | None = None,
74
+ ) -> JSONObject:
75
+ return self.app_publish(profile=profile, app_key=app_key, payload=payload or {})
76
+
77
+ def app_list(self, *, profile: str, ship_auth: bool = False) -> JSONObject:
78
+ """Get all apps with full hierarchy from tag/apps endpoint."""
79
+ def runner(session_profile, context):
80
+ result = self.backend.request("GET", context, "/tag/apps")
81
+ return {
82
+ "profile": profile,
83
+ "ws_id": session_profile.selected_ws_id,
84
+ "items": result,
85
+ }
86
+
87
+ return self._run(profile, runner)
88
+
89
+ def app_search(self, *, profile: str, keyword: str = "", page_num: int = 1, page_size: int = 50) -> JSONObject:
90
+ """Search apps by keyword in name/title using backend search API.
91
+ Useful for finding BUG-related apps across all packages."""
92
+ def runner(session_profile, context):
93
+ # Use GET /app/item which supports queryKey search across all apps
94
+ params: JSONObject = {"pageNum": page_num, "pageSize": page_size}
95
+ if keyword:
96
+ params["queryKey"] = keyword
97
+
98
+ result = self.backend.request("GET", context, "/app/item", params=params)
99
+
100
+ # Extract app list from the response
101
+ apps = []
102
+ if isinstance(result, dict):
103
+ items = result.get("list", [])
104
+ for item in items:
105
+ if isinstance(item, dict):
106
+ apps.append({
107
+ "app_key": item.get("appKey"),
108
+ "title": item.get("title") or item.get("formTitle"),
109
+ "form_id": item.get("formId"),
110
+ "tag_id": item.get("tagId"),
111
+ "group_id": item.get("groupId"),
112
+ })
113
+
114
+ return {
115
+ "profile": profile,
116
+ "ws_id": session_profile.selected_ws_id,
117
+ "keyword": keyword,
118
+ "page_num": page_num,
119
+ "page_size": page_size,
120
+ "total": result.get("total") if isinstance(result, dict) else len(apps),
121
+ "apps": apps,
122
+ }
123
+
124
+ return self._run(profile, runner)
125
+
126
+ def app_get_base(self, *, profile: str, app_key: str, include_raw: bool = False) -> JSONObject:
127
+ self._require_app_key(app_key)
128
+
129
+ def runner(session_profile, context):
130
+ result = self.backend.request("GET", context, f"/app/{app_key}/baseInfo")
131
+ publish_status = result.get("appPublishStatus") if isinstance(result, dict) else None
132
+ compact = self._compact_base_info(result if isinstance(result, dict) else {})
133
+ response = {
134
+ "profile": profile,
135
+ "ws_id": session_profile.selected_ws_id,
136
+ "app_key": app_key,
137
+ "result": result if include_raw else compact,
138
+ "app_publish_status": publish_status,
139
+ "app_publish_status_label": get_app_publish_status_label(publish_status if isinstance(publish_status, int) else None),
140
+ "compact": not include_raw,
141
+ }
142
+ if include_raw:
143
+ response["summary"] = compact
144
+ return response
145
+
146
+ return self._run(profile, runner)
147
+
148
+ def app_update_base(self, *, profile: str, app_key: str, payload: JSONObject) -> JSONObject:
149
+ self._require_app_key(app_key)
150
+ body = self._require_dict(payload)
151
+
152
+ def runner(session_profile, context):
153
+ result = self.backend.request("POST", context, f"/app/{app_key}/baseInfo", json_body=body)
154
+ return self._attach_human_review_notice(
155
+ {"profile": profile, "ws_id": session_profile.selected_ws_id, "app_key": app_key, "result": result},
156
+ operation="update",
157
+ target="app base settings",
158
+ )
159
+
160
+ return self._run(profile, runner)
161
+
162
+ def app_get_form_schema(
163
+ self,
164
+ *,
165
+ profile: str,
166
+ app_key: str,
167
+ form_type: int,
168
+ being_draft: bool | None,
169
+ being_apply: bool | None,
170
+ audit_node_id: int | None,
171
+ include_raw: bool = False,
172
+ ) -> JSONObject:
173
+ self._require_app_key(app_key)
174
+
175
+ def runner(session_profile, context):
176
+ params: JSONObject = {"type": form_type}
177
+ if being_draft is not None:
178
+ params["beingDraft"] = being_draft
179
+ if being_apply is not None:
180
+ params["beingApply"] = being_apply
181
+ if audit_node_id is not None:
182
+ params["auditNodeId"] = audit_node_id
183
+ result = self.backend.request("GET", context, f"/app/{app_key}/form", params=params)
184
+ compact = self._compact_form_schema(result if isinstance(result, dict) else {})
185
+ response = {
186
+ "profile": profile,
187
+ "ws_id": session_profile.selected_ws_id,
188
+ "app_key": app_key,
189
+ "result": result if include_raw else compact,
190
+ "compact": not include_raw,
191
+ }
192
+ if include_raw:
193
+ response["summary"] = compact
194
+ return response
195
+
196
+ return self._run(profile, runner)
197
+
198
+ def app_get_edit_version_no(self, *, profile: str, app_key: str) -> JSONObject:
199
+ self._require_app_key(app_key)
200
+
201
+ def runner(session_profile, context):
202
+ result = self.backend.request("GET", context, f"/app/{app_key}/editVersionNo")
203
+ return {"profile": profile, "ws_id": session_profile.selected_ws_id, "app_key": app_key, "result": result}
204
+
205
+ return self._run(profile, runner)
206
+
207
+ def app_update_form_schema(self, *, profile: str, app_key: str, payload: JSONObject) -> JSONObject:
208
+ self._require_app_key(app_key)
209
+ body = self._require_dict(payload)
210
+
211
+ def runner(session_profile, context):
212
+ result = self.backend.request("POST", context, f"/app/{app_key}/form", json_body=body)
213
+ return self._attach_human_review_notice(
214
+ {"profile": profile, "ws_id": session_profile.selected_ws_id, "app_key": app_key, "result": result},
215
+ operation="update",
216
+ target="app form schema",
217
+ )
218
+
219
+ return self._run(profile, runner)
220
+
221
+ def app_edit_finished(self, *, profile: str, app_key: str, payload: JSONObject) -> JSONObject:
222
+ self._require_app_key(app_key)
223
+ body = self._require_dict(payload)
224
+
225
+ def runner(session_profile, context):
226
+ result = self.backend.request("POST", context, f"/app/{app_key}/editFinished", json_body=body)
227
+ return {"profile": profile, "ws_id": session_profile.selected_ws_id, "app_key": app_key, "result": result}
228
+
229
+ return self._run(profile, runner)
230
+
231
+ def app_create(self, *, profile: str, payload: JSONObject) -> JSONObject:
232
+ body = self._require_dict(payload)
233
+
234
+ def runner(session_profile, context):
235
+ result = self.backend.request("POST", context, "/app", json_body=body)
236
+ return {"profile": profile, "ws_id": session_profile.selected_ws_id, "result": result}
237
+
238
+ return self._run(profile, runner)
239
+
240
+ def app_delete(self, *, profile: str, app_key: str) -> JSONObject:
241
+ self._require_app_key(app_key)
242
+
243
+ def runner(session_profile, context):
244
+ result = self.backend.request("DELETE", context, f"/app/{app_key}")
245
+ return self._attach_human_review_notice(
246
+ {"profile": profile, "ws_id": session_profile.selected_ws_id, "app_key": app_key, "result": result},
247
+ operation="delete",
248
+ target="app configuration",
249
+ )
250
+
251
+ return self._run(profile, runner)
252
+
253
+ def app_publish(self, *, profile: str, app_key: str, payload: JSONObject) -> JSONObject:
254
+ self._require_app_key(app_key)
255
+ body = self._require_dict(payload)
256
+
257
+ def runner(session_profile, context):
258
+ result = self.backend.request("POST", context, f"/app/{app_key}/publish", json_body=body)
259
+ return {"profile": profile, "ws_id": session_profile.selected_ws_id, "app_key": app_key, "result": result}
260
+
261
+ return self._run(profile, runner)
262
+
263
+ def _require_app_key(self, app_key: str) -> None:
264
+ if not app_key:
265
+ raise_tool_error(QingflowApiError.config_error("app_key is required"))
266
+
267
+ def _compact_base_info(self, result: dict[str, Any]) -> JSONObject:
268
+ publish_status = result.get("appPublishStatus")
269
+ auth = result.get("auth") if isinstance(result.get("auth"), dict) else {}
270
+ contact_auth = auth.get("contactAuth") if isinstance(auth, dict) and isinstance(auth.get("contactAuth"), dict) else {}
271
+ external_auth = auth.get("externalMemberAuth") if isinstance(auth, dict) and isinstance(auth.get("externalMemberAuth"), dict) else {}
272
+ tag_items = result.get("tagItems") if isinstance(result.get("tagItems"), list) else []
273
+ tag_ids = result.get("tagIds") if isinstance(result.get("tagIds"), list) else []
274
+ return {
275
+ "appKey": result.get("appKey"),
276
+ "formId": result.get("formId"),
277
+ "formTitle": result.get("formTitle"),
278
+ "appIcon": result.get("appIcon"),
279
+ "appPublishStatus": publish_status,
280
+ "appPublishStatusLabel": get_app_publish_status_label(publish_status if isinstance(publish_status, int) else None),
281
+ "appOpenStatus": result.get("appOpenStatus"),
282
+ "applyOpenStatus": result.get("applyOpenStatus"),
283
+ "dataManageStatus": result.get("dataManageStatus"),
284
+ "editItemStatus": result.get("editItemStatus"),
285
+ "deleteItemStatus": result.get("deleteItemStatus"),
286
+ "copyAppStatus": result.get("copyAppStatus"),
287
+ "flowStatus": result.get("flowStatus"),
288
+ "createTime": result.get("createTime"),
289
+ "creator": self._compact_user(result.get("creator")),
290
+ "tagIds": tag_ids,
291
+ "tagCount": len(tag_ids),
292
+ "tagItemCount": len(tag_items),
293
+ "tagItemsPreview": [self._compact_tag_item(item) for item in tag_items[:10] if isinstance(item, dict)],
294
+ "authSummary": {
295
+ "type": auth.get("type") if isinstance(auth, dict) else None,
296
+ "contactAuthType": contact_auth.get("type") if isinstance(contact_auth, dict) else None,
297
+ "contactDepartments": self._count_auth_members(contact_auth, "depart"),
298
+ "contactMembers": self._count_auth_members(contact_auth, "member"),
299
+ "contactRoles": self._count_auth_members(contact_auth, "role"),
300
+ "contactIncludeSubDeparts": self._extract_include_sub_departs(contact_auth),
301
+ "externalAuthType": external_auth.get("type") if isinstance(external_auth, dict) else None,
302
+ "externalDepartments": self._count_auth_members(external_auth, "depart"),
303
+ "externalMembers": self._count_auth_members(external_auth, "member"),
304
+ "externalRoles": self._count_auth_members(external_auth, "role"),
305
+ },
306
+ }
307
+
308
+ def _compact_form_schema(self, result: dict[str, Any]) -> JSONObject:
309
+ base_questions_raw = result.get("baseQues") if isinstance(result.get("baseQues"), list) else []
310
+ form_questions_raw = result.get("formQues") if isinstance(result.get("formQues"), list) else []
311
+ base_questions = [self._compact_question(question) for question in base_questions_raw if isinstance(question, dict)]
312
+ form_questions = [self._compact_question(question) for question in form_questions_raw if isinstance(question, dict)]
313
+ all_questions = base_questions + form_questions
314
+ type_counts = Counter(
315
+ question["queType"]
316
+ for question in all_questions
317
+ if isinstance(question.get("queType"), int)
318
+ )
319
+ required_count = sum(1 for question in all_questions if question.get("required") is True)
320
+ return {
321
+ "appKey": result.get("appKey"),
322
+ "formId": result.get("formId"),
323
+ "formTitle": result.get("formTitle"),
324
+ "editVersionNo": result.get("editVersionNo"),
325
+ "serialNumType": result.get("serialNumType"),
326
+ "hideCopyright": result.get("hideCopyright"),
327
+ "questionCounts": {
328
+ "baseQuestions": len(base_questions),
329
+ "formQuestions": len(form_questions),
330
+ "totalQuestions": len(all_questions),
331
+ "requiredQuestions": required_count,
332
+ "questionRelations": len(result.get("questionRelations") or []),
333
+ "selectScopeRelations": len(result.get("selectScopeRelations") or []),
334
+ "serialNumConfigs": len(result.get("serialNumConfig") or []),
335
+ },
336
+ "questionTypeCounts": [
337
+ {"queType": que_type, "count": count}
338
+ for que_type, count in sorted(type_counts.items())
339
+ ],
340
+ "baseQuestions": base_questions,
341
+ "formQuestions": form_questions,
342
+ }
343
+
344
+ def _compact_user(self, user: Any) -> JSONObject | None:
345
+ if not isinstance(user, dict):
346
+ return None
347
+ return {
348
+ "uid": user.get("uid"),
349
+ "nickName": user.get("nickName"),
350
+ "email": user.get("email"),
351
+ "remark": user.get("remark"),
352
+ }
353
+
354
+ def _compact_tag_item(self, item: dict[str, Any]) -> JSONObject:
355
+ return {
356
+ "itemType": item.get("itemType"),
357
+ "title": item.get("title"),
358
+ "appKey": item.get("appKey"),
359
+ "formId": item.get("formId"),
360
+ "dashKey": item.get("dashKey"),
361
+ "pageKey": item.get("pageKey"),
362
+ }
363
+
364
+ def _compact_question(self, question: dict[str, Any]) -> JSONObject:
365
+ options = question.get("options")
366
+ inner_questions = question.get("innerQuestions")
367
+ sub_questions = question.get("subQuestions")
368
+ compact = {
369
+ "queId": question.get("queId"),
370
+ "queTitle": question.get("queTitle"),
371
+ "queType": question.get("queType"),
372
+ "required": question.get("required") is True,
373
+ "ordinal": question.get("ordinal"),
374
+ "inlineOrdinal": question.get("inlineOrdinal"),
375
+ "queWidth": question.get("queWidth"),
376
+ "sectionId": question.get("sectionId"),
377
+ "supId": question.get("supId"),
378
+ "relatedQueId": question.get("relatedQueId"),
379
+ "pluginStatus": question.get("pluginStatus"),
380
+ "optionCount": len(options) if isinstance(options, list) else 0,
381
+ "innerQuestionCount": len(inner_questions) if isinstance(inner_questions, list) else 0,
382
+ "subQuestionCount": len(sub_questions) if isinstance(sub_questions, list) else 0,
383
+ }
384
+ return {key: value for key, value in compact.items() if value is not None}
385
+
386
+ def _count_auth_members(self, auth_payload: Any, member_key: str) -> int:
387
+ if not isinstance(auth_payload, dict):
388
+ return 0
389
+ auth_members = auth_payload.get("authMembers")
390
+ if not isinstance(auth_members, dict):
391
+ return 0
392
+ members = auth_members.get(member_key)
393
+ return len(members) if isinstance(members, list) else 0
394
+
395
+ def _extract_include_sub_departs(self, auth_payload: Any) -> bool | None:
396
+ if not isinstance(auth_payload, dict):
397
+ return None
398
+ auth_members = auth_payload.get("authMembers")
399
+ if isinstance(auth_members, dict):
400
+ include_sub_departs = auth_members.get("includeSubDeparts")
401
+ if isinstance(include_sub_departs, bool):
402
+ return include_sub_departs
403
+ include_sub_departs = auth_payload.get("includeSubDeparts")
404
+ if isinstance(include_sub_departs, bool):
405
+ return include_sub_departs
406
+ return None