@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.
- package/README.md +5 -3
- package/docs/local-agent-install.md +21 -5
- package/npm/bin/qingflow-app-builder-mcp.mjs +1 -1
- package/npm/lib/runtime.mjs +168 -12
- package/package.json +1 -1
- package/pyproject.toml +4 -1
- package/skills/qingflow-app-builder/SKILL.md +155 -22
- package/skills/qingflow-app-builder/references/create-app.md +51 -21
- package/skills/qingflow-app-builder/references/environments.md +1 -1
- package/skills/qingflow-app-builder/references/flow-actors-and-permissions.md +123 -0
- package/skills/qingflow-app-builder/references/gotchas.md +28 -1
- package/skills/qingflow-app-builder/references/solution-playbooks.md +14 -12
- package/skills/qingflow-app-builder/references/tool-selection.md +47 -19
- package/skills/qingflow-app-builder/references/update-flow.md +112 -25
- package/skills/qingflow-app-builder/references/update-layout.md +11 -24
- package/skills/qingflow-app-builder/references/update-schema.md +1 -23
- package/skills/qingflow-app-builder/references/update-views.md +87 -21
- package/skills/qingflow-app-builder-code-integrations/SKILL.md +137 -0
- package/skills/qingflow-app-builder-code-integrations/agents/openai.yaml +4 -0
- package/skills/qingflow-app-builder-code-integrations/references/code-block.md +66 -0
- package/skills/qingflow-app-builder-code-integrations/references/q-linker.md +77 -0
- package/src/qingflow_mcp/__init__.py +1 -1
- package/src/qingflow_mcp/backend_client.py +210 -0
- package/src/qingflow_mcp/builder_facade/models.py +1252 -3
- package/src/qingflow_mcp/builder_facade/service.py +11367 -2389
- package/src/qingflow_mcp/cli/__init__.py +1 -0
- package/src/qingflow_mcp/cli/commands/__init__.py +15 -0
- package/src/qingflow_mcp/cli/commands/app.py +40 -0
- package/src/qingflow_mcp/cli/commands/auth.py +78 -0
- package/src/qingflow_mcp/cli/commands/builder.py +515 -0
- package/src/qingflow_mcp/cli/commands/common.py +62 -0
- package/src/qingflow_mcp/cli/commands/imports.py +96 -0
- package/src/qingflow_mcp/cli/commands/record.py +304 -0
- package/src/qingflow_mcp/cli/commands/task.py +89 -0
- package/src/qingflow_mcp/cli/commands/workspace.py +33 -0
- package/src/qingflow_mcp/cli/context.py +48 -0
- package/src/qingflow_mcp/cli/formatters.py +355 -0
- package/src/qingflow_mcp/cli/json_io.py +50 -0
- package/src/qingflow_mcp/cli/main.py +149 -0
- package/src/qingflow_mcp/config.py +39 -0
- package/src/qingflow_mcp/import_store.py +121 -0
- package/src/qingflow_mcp/list_type_labels.py +24 -0
- package/src/qingflow_mcp/response_trim.py +668 -0
- package/src/qingflow_mcp/server.py +160 -18
- package/src/qingflow_mcp/server_app_builder.py +275 -68
- package/src/qingflow_mcp/server_app_user.py +219 -191
- package/src/qingflow_mcp/session_store.py +41 -1
- package/src/qingflow_mcp/solution/compiler/form_compiler.py +43 -4
- package/src/qingflow_mcp/solution/compiler/icon_utils.py +119 -45
- package/src/qingflow_mcp/solution/compiler/workflow_compiler.py +41 -2
- package/src/qingflow_mcp/solution/executor.py +107 -11
- package/src/qingflow_mcp/solution/spec_models.py +2 -0
- package/src/qingflow_mcp/tools/ai_builder_tools.py +2032 -127
- package/src/qingflow_mcp/tools/app_tools.py +419 -12
- package/src/qingflow_mcp/tools/approval_tools.py +571 -72
- package/src/qingflow_mcp/tools/auth_tools.py +398 -2
- package/src/qingflow_mcp/tools/code_block_tools.py +756 -0
- package/src/qingflow_mcp/tools/custom_button_tools.py +179 -0
- package/src/qingflow_mcp/tools/directory_tools.py +203 -31
- package/src/qingflow_mcp/tools/feedback_tools.py +230 -0
- package/src/qingflow_mcp/tools/file_tools.py +1 -0
- package/src/qingflow_mcp/tools/import_tools.py +2150 -0
- package/src/qingflow_mcp/tools/package_tools.py +18 -4
- package/src/qingflow_mcp/tools/portal_tools.py +31 -0
- package/src/qingflow_mcp/tools/qingbi_report_tools.py +109 -7
- package/src/qingflow_mcp/tools/record_tools.py +9894 -1104
- package/src/qingflow_mcp/tools/solution_tools.py +115 -3
- package/src/qingflow_mcp/tools/task_context_tools.py +2040 -0
- package/src/qingflow_mcp/tools/task_tools.py +376 -225
- 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
|
-
|
|
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.
|
|
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
|
-
"
|
|
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
|
-
|
|
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 = {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
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 []
|