@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
@@ -62,7 +62,11 @@ class PackageTools(ToolBase):
62
62
 
63
63
  def runner(session_profile, context):
64
64
  result = self.backend.request("GET", context, f"/tag/{tag_id}")
65
- compact = self._compact_package(result if isinstance(result, dict) else {})
65
+ base_info = self.backend.request("GET", context, f"/tag/{tag_id}/baseInfo")
66
+ compact = self._compact_package(
67
+ result if isinstance(result, dict) else {},
68
+ base_info=base_info if isinstance(base_info, dict) else None,
69
+ )
66
70
  response = {
67
71
  "profile": profile,
68
72
  "ws_id": session_profile.selected_ws_id,
@@ -77,7 +81,7 @@ class PackageTools(ToolBase):
77
81
  return self._run(profile, runner)
78
82
 
79
83
  def package_get_base(self, *, profile: str, tag_id: int, include_raw: bool = False) -> JSONObject:
80
- self._require_tag_id(tag_id)
84
+ self._require_readable_tag_id(tag_id)
81
85
 
82
86
  def runner(session_profile, context):
83
87
  result = self.backend.request("GET", context, f"/tag/{tag_id}/baseInfo")
@@ -160,6 +164,10 @@ class PackageTools(ToolBase):
160
164
  if tag_id <= 0:
161
165
  raise_tool_error(QingflowApiError.config_error("tag_id must be positive"))
162
166
 
167
+ def _require_readable_tag_id(self, tag_id: int) -> None:
168
+ if tag_id < 0:
169
+ raise_tool_error(QingflowApiError.config_error("tag_id must be non-negative"))
170
+
163
171
  def _normalize_package_payload(self, payload: JSONObject, existing: JSONObject | None = None) -> JSONObject:
164
172
  data = deepcopy(existing) if isinstance(existing, dict) else {}
165
173
  data.update(deepcopy(payload))
@@ -174,8 +182,9 @@ class PackageTools(ToolBase):
174
182
  data.setdefault("auth", _default_package_auth())
175
183
  return data
176
184
 
177
- def _compact_package(self, result: dict[str, object]) -> JSONObject:
185
+ def _compact_package(self, result: dict[str, object], *, base_info: dict[str, object] | None = None) -> JSONObject:
178
186
  tag_items = result.get("tagItems") if isinstance(result.get("tagItems"), list) else []
187
+ permission_source = base_info if isinstance(base_info, dict) else result
179
188
  preview = []
180
189
  for item in tag_items[:10]:
181
190
  if not isinstance(item, dict):
@@ -191,10 +200,15 @@ class PackageTools(ToolBase):
191
200
  compact = {
192
201
  "tagId": result.get("tagId"),
193
202
  "tagName": result.get("tagName"),
194
- "publishStatus": result.get("publishStatus"),
203
+ "tagIcon": result.get("tagIcon") if result.get("tagIcon") is not None else permission_source.get("tagIcon"),
204
+ "publishStatus": result.get("publishStatus") if result.get("publishStatus") is not None else permission_source.get("publishStatus"),
195
205
  "beingTrial": result.get("beingTrial"),
196
206
  "itemCount": len(tag_items),
197
207
  "itemPreview": preview,
208
+ "addAppStatus": permission_source.get("addAppStatus"),
209
+ "editAppStatus": permission_source.get("editAppStatus"),
210
+ "delAppStatus": permission_source.get("delAppStatus"),
211
+ "editTagStatus": permission_source.get("editTagStatus"),
198
212
  }
199
213
  return {key: value for key, value in compact.items() if value is not None}
200
214
 
@@ -18,10 +18,18 @@ class PortalTools(ToolBase):
18
18
  def portal_get(profile: str = DEFAULT_PROFILE, dash_key: str = "", being_draft: bool = False) -> JSONObject:
19
19
  return self.portal_get(profile=profile, dash_key=dash_key, being_draft=being_draft)
20
20
 
21
+ @mcp.tool()
22
+ def portal_get_base_info(profile: str = DEFAULT_PROFILE, dash_key: str = "") -> JSONObject:
23
+ return self.portal_get_base_info(profile=profile, dash_key=dash_key)
24
+
21
25
  @mcp.tool()
22
26
  def portal_create(profile: str = DEFAULT_PROFILE, payload: JSONObject | None = None) -> JSONObject:
23
27
  return self.portal_create(profile=profile, payload=payload or {})
24
28
 
29
+ @mcp.tool(description=self._high_risk_tool_description(operation="update", target="portal base settings"))
30
+ def portal_update_base_info(profile: str = DEFAULT_PROFILE, dash_key: str = "", payload: JSONObject | None = None) -> JSONObject:
31
+ return self.portal_update_base_info(profile=profile, dash_key=dash_key, payload=payload or {})
32
+
25
33
  @mcp.tool(description=self._high_risk_tool_description(operation="update", target="portal configuration"))
26
34
  def portal_update(profile: str = DEFAULT_PROFILE, dash_key: str = "", payload: JSONObject | None = None) -> JSONObject:
27
35
  return self.portal_update(profile=profile, dash_key=dash_key, payload=payload or {})
@@ -59,6 +67,29 @@ class PortalTools(ToolBase):
59
67
 
60
68
  return self._run(profile, runner)
61
69
 
70
+ def portal_get_base_info(self, *, profile: str, dash_key: str) -> JSONObject:
71
+ self._require_dash_key(dash_key)
72
+
73
+ def runner(session_profile, context):
74
+ result = self.backend.request("GET", context, f"/dash/{dash_key}/baseInfo")
75
+ return {"profile": profile, "ws_id": session_profile.selected_ws_id, "dash_key": dash_key, "result": result}
76
+
77
+ return self._run(profile, runner)
78
+
79
+ def portal_update_base_info(self, *, profile: str, dash_key: str, payload: JSONObject) -> JSONObject:
80
+ self._require_dash_key(dash_key)
81
+ body = self._require_dict(payload)
82
+
83
+ def runner(session_profile, context):
84
+ result = self.backend.request("POST", context, f"/dash/{dash_key}/baseInfo", json_body=body)
85
+ return self._attach_human_review_notice(
86
+ {"profile": profile, "ws_id": session_profile.selected_ws_id, "dash_key": dash_key, "result": result},
87
+ operation="update",
88
+ target="portal base settings",
89
+ )
90
+
91
+ return self._run(profile, runner)
92
+
62
93
  def portal_update(self, *, profile: str, dash_key: str, payload: JSONObject) -> JSONObject:
63
94
  self._require_dash_key(dash_key)
64
95
  body = self._require_dict(payload)
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import json
3
4
  from uuid import uuid4
4
5
 
5
6
  from mcp.server.fastmcp import FastMCP
@@ -18,6 +19,37 @@ def _qingbi_base_url(base_url: str) -> str:
18
19
  return normalized[:-4] if normalized.endswith("/api") else normalized
19
20
 
20
21
 
22
+ def _should_retry_qflow_base(error: QingflowApiError) -> bool:
23
+ return int(getattr(error, "backend_code", 0) or 0) == 81007
24
+
25
+
26
+ def _should_retry_asos_data(error: QingflowApiError) -> bool:
27
+ backend_code = int(getattr(error, "backend_code", 0) or 0)
28
+ http_status = getattr(error, "http_status", None)
29
+ return backend_code in {44011, 81007} or http_status == 404
30
+
31
+
32
+ def _coerce_tool_error(error: RuntimeError | QingflowApiError) -> QingflowApiError | None:
33
+ if isinstance(error, QingflowApiError):
34
+ return error
35
+ if not isinstance(error, RuntimeError):
36
+ return None
37
+ try:
38
+ payload = json.loads(str(error))
39
+ except Exception:
40
+ return None
41
+ if not isinstance(payload, dict):
42
+ return None
43
+ return QingflowApiError(
44
+ category=str(payload.get("category") or "runtime"),
45
+ message=str(payload.get("message") or str(error)),
46
+ backend_code=payload.get("backend_code"),
47
+ request_id=payload.get("request_id"),
48
+ http_status=payload.get("http_status"),
49
+ details=payload.get("details") if isinstance(payload.get("details"), dict) else None,
50
+ )
51
+
52
+
21
53
  class QingbiReportTools(ToolBase):
22
54
  def register(self, mcp: FastMCP) -> None:
23
55
  @mcp.tool()
@@ -95,13 +127,45 @@ class QingbiReportTools(ToolBase):
95
127
  self._require_app_key(app_key)
96
128
  return self._request(profile, "GET", f"/qingbi/charts/data/bichart/{app_key}", app_key=app_key, items_key="items")
97
129
 
130
+ def qingbi_report_list_sorted(
131
+ self,
132
+ *,
133
+ profile: str,
134
+ app_key: str,
135
+ page_num: int = 1,
136
+ page_size: int = 500,
137
+ search_key: str | None = None,
138
+ ) -> JSONObject:
139
+ self._require_app_key(app_key)
140
+ body: JSONObject = {"appKey": app_key, "pageNum": page_num, "pageSize": page_size}
141
+ if search_key:
142
+ body["searchKey"] = search_key
143
+ return self._request(
144
+ profile,
145
+ "POST",
146
+ "/qingbi/charts/chart/list",
147
+ app_key=app_key,
148
+ page_num=page_num,
149
+ page_size=page_size,
150
+ search_key=search_key,
151
+ items_key="items",
152
+ result_transform=_extract_sorted_chart_items,
153
+ json_body=body,
154
+ )
155
+
98
156
  def qingbi_report_create(self, *, profile: str, payload: JSONObject) -> JSONObject:
99
157
  body = self._require_dict(payload)
100
158
  return self._request(profile, "POST", "/qingbi/charts", json_body=body)
101
159
 
102
160
  def qingbi_report_get_base(self, *, profile: str, chart_id: str) -> JSONObject:
103
161
  self._require_chart_id(chart_id)
104
- return self._request(profile, "GET", f"/qingbi/charts/baseinfo/{chart_id}", chart_id=chart_id)
162
+ try:
163
+ return self._request(profile, "GET", f"/qingbi/charts/baseinfo/{chart_id}", chart_id=chart_id)
164
+ except (QingflowApiError, RuntimeError) as raw_error:
165
+ error = _coerce_tool_error(raw_error)
166
+ if error is None or not _should_retry_qflow_base(error):
167
+ raise
168
+ return self._request(profile, "GET", f"/qingbi/charts/qflow/baseinfo/{chart_id}", chart_id=chart_id)
105
169
 
106
170
  def qingbi_report_update_base(self, *, profile: str, chart_id: str, payload: JSONObject) -> JSONObject:
107
171
  self._require_chart_id(chart_id)
@@ -129,16 +193,46 @@ class QingbiReportTools(ToolBase):
129
193
  page_size_y: int | None = None,
130
194
  ) -> JSONObject:
131
195
  self._require_chart_id(chart_id)
132
- params = {"qfUUID": uuid4().hex}
133
- if page_num is not None:
134
- params["pageNum"] = page_num
135
- if page_size is not None:
136
- params["pageSize"] = page_size
196
+ params = {
197
+ "qfUUID": uuid4().hex,
198
+ # Match Qingflow's real BI/qflow read path defaults so a caller can
199
+ # fetch chart data without having to synthesize pagination first.
200
+ "pageNum": page_num if page_num is not None else 1,
201
+ "pageSize": page_size if page_size is not None else 20,
202
+ }
137
203
  if page_num_y is not None:
138
204
  params["pageNumY"] = page_num_y
139
205
  if page_size_y is not None:
140
206
  params["pageSizeY"] = page_size_y
141
- return self._request(profile, "POST", f"/qingbi/charts/data/{chart_id}", chart_id=chart_id, params=params, json_body=payload)
207
+ try:
208
+ if payload:
209
+ return self._request(
210
+ profile,
211
+ "POST",
212
+ f"/qingbi/charts/data/qflow/{chart_id}/detail",
213
+ chart_id=chart_id,
214
+ params=params,
215
+ json_body=payload,
216
+ )
217
+ return self._request(
218
+ profile,
219
+ "GET",
220
+ f"/qingbi/charts/data/qflow/{chart_id}",
221
+ chart_id=chart_id,
222
+ params=params,
223
+ )
224
+ except (QingflowApiError, RuntimeError) as raw_error:
225
+ error = _coerce_tool_error(raw_error)
226
+ if error is None or not _should_retry_asos_data(error):
227
+ raise
228
+ return self._request(
229
+ profile,
230
+ "POST",
231
+ f"/qingbi/charts/data/qflow/{chart_id}/asos",
232
+ chart_id=chart_id,
233
+ params=params,
234
+ json_body=payload or {},
235
+ )
142
236
 
143
237
  def qingbi_report_delete(self, *, profile: str, chart_id: str) -> JSONObject:
144
238
  self._require_chart_id(chart_id)
@@ -233,3 +327,11 @@ def _extract_dataset_fields(result: JSONValue) -> list[JSONObject]:
233
327
  if isinstance(result, list):
234
328
  return result
235
329
  return []
330
+
331
+
332
+ def _extract_sorted_chart_items(result: JSONValue) -> list[JSONObject]:
333
+ if isinstance(result, dict) and isinstance(result.get("list"), list):
334
+ return result["list"]
335
+ if isinstance(result, list):
336
+ return result
337
+ return []