@qingflow-tech/qingflow-app-builder-mcp 1.0.2 → 1.0.3

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 (42) hide show
  1. package/README.md +2 -2
  2. package/docs/local-agent-install.md +9 -3
  3. package/npm/lib/runtime.mjs +10 -3
  4. package/package.json +1 -1
  5. package/pyproject.toml +1 -1
  6. package/skills/qingflow-app-builder/SKILL.md +88 -184
  7. package/skills/qingflow-app-builder/references/create-app.md +15 -34
  8. package/skills/qingflow-app-builder/references/gotchas.md +3 -3
  9. package/skills/qingflow-app-builder/references/solution-playbooks.md +1 -2
  10. package/skills/qingflow-app-builder/references/tool-selection.md +9 -10
  11. package/src/qingflow_mcp/__init__.py +33 -1
  12. package/src/qingflow_mcp/builder_facade/models.py +14 -4
  13. package/src/qingflow_mcp/builder_facade/service.py +1582 -124
  14. package/src/qingflow_mcp/cli/commands/auth.py +63 -0
  15. package/src/qingflow_mcp/cli/commands/builder.py +4 -3
  16. package/src/qingflow_mcp/cli/commands/record.py +5 -5
  17. package/src/qingflow_mcp/cli/commands/task.py +74 -22
  18. package/src/qingflow_mcp/cli/commands/workspace.py +22 -0
  19. package/src/qingflow_mcp/cli/formatters.py +287 -48
  20. package/src/qingflow_mcp/cli/main.py +6 -1
  21. package/src/qingflow_mcp/cli/qingflow_login.py +116 -0
  22. package/src/qingflow_mcp/config.py +1 -1
  23. package/src/qingflow_mcp/errors.py +2 -2
  24. package/src/qingflow_mcp/id_utils.py +49 -0
  25. package/src/qingflow_mcp/public_surface.py +11 -1
  26. package/src/qingflow_mcp/response_trim.py +380 -9
  27. package/src/qingflow_mcp/server.py +4 -0
  28. package/src/qingflow_mcp/server_app_builder.py +11 -1
  29. package/src/qingflow_mcp/server_app_user.py +24 -0
  30. package/src/qingflow_mcp/session_store.py +69 -15
  31. package/src/qingflow_mcp/solution/compiler/form_compiler.py +2 -2
  32. package/src/qingflow_mcp/solution/executor.py +2 -2
  33. package/src/qingflow_mcp/tools/ai_builder_tools.py +48 -18
  34. package/src/qingflow_mcp/tools/app_tools.py +1 -0
  35. package/src/qingflow_mcp/tools/auth_tools.py +217 -9
  36. package/src/qingflow_mcp/tools/base.py +6 -2
  37. package/src/qingflow_mcp/tools/code_block_tools.py +2 -2
  38. package/src/qingflow_mcp/tools/import_tools.py +36 -2
  39. package/src/qingflow_mcp/tools/record_tools.py +410 -156
  40. package/src/qingflow_mcp/tools/resource_read_tools.py +114 -32
  41. package/src/qingflow_mcp/tools/task_context_tools.py +899 -141
  42. package/src/qingflow_mcp/tools/workspace_tools.py +141 -0
@@ -35,6 +35,26 @@ class WorkspaceTools(ToolBase):
35
35
  include_external=include_external,
36
36
  )
37
37
 
38
+ @mcp.tool()
39
+ def workspace_get(
40
+ profile: str = DEFAULT_PROFILE,
41
+ ws_id: int = 0,
42
+ ) -> dict[str, Any]:
43
+ return self.workspace_get(
44
+ profile=profile,
45
+ ws_id=ws_id if ws_id > 0 else None,
46
+ )
47
+
48
+ @mcp.tool()
49
+ def workspace_select(
50
+ profile: str = DEFAULT_PROFILE,
51
+ ws_id: int = 0,
52
+ ) -> dict[str, Any]:
53
+ return self.workspace_select(
54
+ profile=profile,
55
+ ws_id=ws_id,
56
+ )
57
+
38
58
  @mcp.tool()
39
59
  def workspace_set_plugin_status(
40
60
  profile: str = DEFAULT_PROFILE,
@@ -93,6 +113,68 @@ class WorkspaceTools(ToolBase):
93
113
 
94
114
  return self._run(profile, runner, require_workspace=False)
95
115
 
116
+ @tool_cn_name("工作区详情")
117
+ def workspace_get(
118
+ self,
119
+ *,
120
+ profile: str = DEFAULT_PROFILE,
121
+ ws_id: int | None = None,
122
+ ) -> dict[str, Any]:
123
+ """读取单个工作区详情,并尽量补齐真实 systemVersion。"""
124
+
125
+ def runner(session_profile, context):
126
+ target_ws_id = ws_id or (session_profile.selected_ws_id if session_profile is not None else None)
127
+ if target_ws_id is None or target_ws_id <= 0:
128
+ raise_tool_error(QingflowApiError.workspace_not_selected(profile))
129
+ workspace = self._fetch_workspace_with_fallback(context, ws_id=target_ws_id)
130
+ system_version = self._workspace_system_version(workspace)
131
+ return {
132
+ "profile": profile,
133
+ "ws_id": target_ws_id,
134
+ "qf_version": system_version,
135
+ "qf_version_source": "workspace_system_version" if system_version else "unverified",
136
+ "workspace": workspace,
137
+ }
138
+
139
+ return self._run(profile, runner, require_workspace=False)
140
+
141
+ @tool_cn_name("切换工作区")
142
+ def workspace_select(
143
+ self,
144
+ *,
145
+ profile: str = DEFAULT_PROFILE,
146
+ ws_id: int,
147
+ ) -> dict[str, Any]:
148
+ """切换当前 profile 选中的工作区,并尽量同步真实 systemVersion。"""
149
+ if ws_id <= 0:
150
+ raise_tool_error(QingflowApiError.config_error("ws_id must be positive"))
151
+
152
+ def runner(_, context):
153
+ workspace = self._fetch_workspace_with_fallback(context, ws_id=ws_id)
154
+ workspace_name = str(workspace.get("workspaceName") or workspace.get("wsName") or workspace.get("remark") or "").strip() or None
155
+ selected = self.sessions.select_workspace(profile, ws_id=ws_id, ws_name=workspace_name)
156
+ system_version = self._workspace_system_version(workspace)
157
+ qf_version_source = "workspace_system_version" if system_version else "unverified"
158
+ if system_version:
159
+ selected = self.sessions.update_route(
160
+ profile,
161
+ qf_version=system_version,
162
+ qf_version_source=qf_version_source,
163
+ )
164
+ return {
165
+ "profile": profile,
166
+ "ws_id": ws_id,
167
+ "qf_version": selected.qf_version,
168
+ "qf_version_source": selected.qf_version_source or qf_version_source,
169
+ "workspace": workspace,
170
+ "selected": {
171
+ "ws_id": selected.selected_ws_id,
172
+ "workspace_name": selected.selected_ws_name,
173
+ },
174
+ }
175
+
176
+ return self._run(profile, runner, require_workspace=False)
177
+
96
178
  @tool_cn_name("设置工作区插件状态")
97
179
  def workspace_set_plugin_status(
98
180
  self,
@@ -123,3 +205,62 @@ class WorkspaceTools(ToolBase):
123
205
  }
124
206
 
125
207
  return self._run(profile, runner)
208
+
209
+ def _fetch_workspace_with_fallback(
210
+ self,
211
+ context: BackendRequestContext,
212
+ *,
213
+ ws_id: int,
214
+ ) -> dict[str, Any]:
215
+ workspace = self.backend.request("GET", context, f"/user/workspace/{ws_id}")
216
+ if not isinstance(workspace, dict):
217
+ raise_tool_error(QingflowApiError(category="workspace", message=f"Workspace {ws_id} is not accessible"))
218
+ if self._workspace_needs_list_fallback(workspace):
219
+ fallback = self._fetch_workspace_from_list(context, ws_id=ws_id)
220
+ if isinstance(fallback, dict):
221
+ merged = dict(workspace)
222
+ for key, value in fallback.items():
223
+ if merged.get(key) in (None, "") and value not in (None, ""):
224
+ merged[key] = value
225
+ workspace = merged
226
+ return workspace
227
+
228
+ def _fetch_workspace_from_list(self, context: BackendRequestContext, *, ws_id: int) -> dict[str, Any] | None:
229
+ payload = self.backend.request(
230
+ "POST",
231
+ BackendRequestContext(
232
+ base_url=context.base_url,
233
+ token=context.token,
234
+ ws_id=None,
235
+ qf_version=context.qf_version,
236
+ qf_version_source=context.qf_version_source,
237
+ ),
238
+ "/user/workspaceList/pageQuery",
239
+ json_body={"pageNum": 1, "pageSize": 100, "authList": [0, 1, 2, 3]},
240
+ )
241
+ workspaces = payload.get("list") if isinstance(payload, dict) else []
242
+ if not isinstance(workspaces, list):
243
+ return None
244
+ found = next(
245
+ (
246
+ item
247
+ for item in workspaces
248
+ if isinstance(item, dict) and item.get("wsId") == ws_id
249
+ ),
250
+ None,
251
+ )
252
+ return found if isinstance(found, dict) else None
253
+
254
+ def _workspace_needs_list_fallback(self, workspace: dict[str, Any]) -> bool:
255
+ workspace_name = str(workspace.get("workspaceName") or workspace.get("wsName") or "").strip()
256
+ system_version = self._workspace_system_version(workspace)
257
+ return not workspace_name or system_version is None
258
+
259
+ def _workspace_system_version(self, workspace: Any) -> str | None:
260
+ if not isinstance(workspace, dict):
261
+ return None
262
+ value = workspace.get("systemVersion")
263
+ if value is None:
264
+ return None
265
+ normalized = str(value).strip()
266
+ return normalized or None