@qingflow-tech/qingflow-cli 1.1.38 → 1.1.40

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 (31) hide show
  1. package/README.md +7 -3
  2. package/package.json +1 -1
  3. package/pyproject.toml +1 -1
  4. package/src/qingflow_mcp/builder_facade/service.py +8 -8
  5. package/src/qingflow_mcp/server_cloud.py +89 -3
  6. package/src/qingflow_mcp/tools/ai_builder_tools.py +1 -59
  7. package/src/qingflow_mcp/tools/app_tools.py +15 -29
  8. package/src/qingflow_mcp/tools/approval_tools.py +16 -40
  9. package/src/qingflow_mcp/tools/auth_tools.py +1 -4
  10. package/src/qingflow_mcp/tools/base.py +16 -34
  11. package/src/qingflow_mcp/tools/code_block_tools.py +2 -5
  12. package/src/qingflow_mcp/tools/custom_button_tools.py +6 -11
  13. package/src/qingflow_mcp/tools/directory_tools.py +9 -16
  14. package/src/qingflow_mcp/tools/export_tools.py +5 -9
  15. package/src/qingflow_mcp/tools/feedback_tools.py +0 -2
  16. package/src/qingflow_mcp/tools/file_tools.py +3 -5
  17. package/src/qingflow_mcp/tools/import_tools.py +7 -13
  18. package/src/qingflow_mcp/tools/navigation_tools.py +12 -23
  19. package/src/qingflow_mcp/tools/package_tools.py +11 -21
  20. package/src/qingflow_mcp/tools/portal_tools.py +9 -17
  21. package/src/qingflow_mcp/tools/qingbi_report_tools.py +18 -23
  22. package/src/qingflow_mcp/tools/record_tools.py +97 -53
  23. package/src/qingflow_mcp/tools/repository_dev_tools.py +4 -7
  24. package/src/qingflow_mcp/tools/resource_read_tools.py +6 -9
  25. package/src/qingflow_mcp/tools/role_tools.py +5 -9
  26. package/src/qingflow_mcp/tools/solution_tools.py +4 -16
  27. package/src/qingflow_mcp/tools/task_context_tools.py +6 -12
  28. package/src/qingflow_mcp/tools/task_tools.py +12 -29
  29. package/src/qingflow_mcp/tools/view_tools.py +23 -32
  30. package/src/qingflow_mcp/tools/workflow_tools.py +30 -47
  31. package/src/qingflow_mcp/tools/workspace_tools.py +5 -9
package/README.md CHANGED
@@ -1,9 +1,13 @@
1
1
  # Qingflow CLI
2
2
 
3
+ Qingflow CLI 是轻流面向开发者和 AI Agent 的命令行工具。它把轻流工作区、应用、表单数据、任务、视图、图表、门户和应用搭建能力封装成可脚本化的命令,适合在本地终端、自动化脚本、Codex、Claude Desktop 等 agent 环境中使用。
4
+
5
+ 你可以用它完成登录鉴权、查询工作区和应用结构、读写记录、导入导出数据、处理任务上下文,也可以通过 builder 能力批量创建或更新轻流应用、视图、图表、门户和流程配置。
6
+
3
7
  ## 安装
4
8
 
5
9
  ```bash
6
- npm install @qingflow-tech/qingflow-cli@1.1.38
10
+ npm install @qingflow-tech/qingflow-cli@1.1.40
7
11
  ```
8
12
 
9
13
  推荐从官网复制这一条一键安装 Qingflow CLI:
@@ -17,7 +21,7 @@ npx qingflow-cli@latest install
17
21
  ## 运行
18
22
 
19
23
  ```bash
20
- npx -y -p @qingflow-tech/qingflow-cli@1.1.38 qingflow
24
+ npx -y -p @qingflow-tech/qingflow-cli@1.1.40 qingflow
21
25
  ```
22
26
 
23
27
  全局安装后可以直接运行:
@@ -51,7 +55,7 @@ qingflow-skills install --agent codex --scope user
51
55
  如果你通过一次性 `npx -p` 执行 skills 安装,建议加 `--copy`,避免 symlink 指向 npm 临时执行缓存:
52
56
 
53
57
  ```bash
54
- npx -y -p @qingflow-tech/qingflow-cli@1.1.38 qingflow-skills install --agent codex --scope user --copy
58
+ npx -y -p @qingflow-tech/qingflow-cli@1.1.40 qingflow-skills install --agent codex --scope user --copy
55
59
  ```
56
60
 
57
61
  `qingflow-skills install` 默认使用 symlink,且遇到同名 skill 不会覆盖;只有显式传入 `--force` 才会替换。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qingflow-tech/qingflow-cli",
3
- "version": "1.1.38",
3
+ "version": "1.1.40",
4
4
  "description": "Human-friendly Qingflow command line interface for auth, record operations, import, tasks, and stable builder flows.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/pyproject.toml CHANGED
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "qingflow-mcp"
7
- version = "1.1.38"
7
+ version = "1.1.40"
8
8
  description = "User-authenticated MCP server for Qingflow"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -294,8 +294,8 @@ class AiBuilderFacade:
294
294
  self.directory = directory
295
295
  self.solutions = solutions
296
296
 
297
- def _with_backend_context(self, profile: str, handler):
298
- return self.apps._run(profile, lambda _session_profile, context: handler(context))
297
+ def _with_backend_context(self, profile: str, handler, *, tool_name: str = "搭建后台上下文"):
298
+ return self.apps._run(profile, lambda _session_profile, context: handler(context), tool_name=tool_name)
299
299
 
300
300
  def _read_app_base_raw_direct(self, *, profile: str, app_key: str) -> JSONObject:
301
301
  return self._with_backend_context(
@@ -7392,7 +7392,7 @@ class AiBuilderFacade:
7392
7392
  )
7393
7393
  return _normalize_associated_resources_payload(payload, include_raw=include_raw)
7394
7394
 
7395
- return self.apps._run(profile, runner)
7395
+ return self.apps._run(profile, runner, tool_name="关联资源读取")
7396
7396
 
7397
7397
  def _match_field_index_for_app(self, *, profile: str, app_key: str) -> dict[int, dict[str, Any]]:
7398
7398
  state = self._load_base_schema_state(profile=profile, app_key=app_key)
@@ -7466,7 +7466,7 @@ class AiBuilderFacade:
7466
7466
  result = self.apps.backend.request("POST", context, f"/app/{app_key}/asosChart", json_body=payload)
7467
7467
  return result if isinstance(result, dict) else {"result": result}
7468
7468
 
7469
- return self.apps._run(profile, runner)
7469
+ return self.apps._run(profile, runner, tool_name="关联资源创建")
7470
7470
 
7471
7471
  def _associated_resource_update(
7472
7472
  self,
@@ -7489,21 +7489,21 @@ class AiBuilderFacade:
7489
7489
  result = self.apps.backend.request("POST", context, f"/app/{app_key}/asosChart/{associated_item_id}", json_body=payload)
7490
7490
  return result if isinstance(result, dict) else {"result": result}
7491
7491
 
7492
- return self.apps._run(profile, runner)
7492
+ return self.apps._run(profile, runner, tool_name="关联资源更新")
7493
7493
 
7494
7494
  def _associated_resource_delete(self, *, profile: str, app_key: str, associated_item_id: int) -> JSONObject:
7495
7495
  def runner(_: Any, context: BackendRequestContext) -> JSONObject:
7496
7496
  result = self.apps.backend.request("DELETE", context, f"/app/{app_key}/asosChart/{associated_item_id}")
7497
7497
  return result if isinstance(result, dict) else {"result": result}
7498
7498
 
7499
- return self.apps._run(profile, runner)
7499
+ return self.apps._run(profile, runner, tool_name="关联资源删除")
7500
7500
 
7501
7501
  def _associated_resource_reorder(self, *, profile: str, app_key: str, associated_item_ids: list[int]) -> JSONObject:
7502
7502
  def runner(_: Any, context: BackendRequestContext) -> JSONObject:
7503
7503
  result = self.apps.backend.request("POST", context, f"/app/{app_key}/asosChart/ordinal", json_body=list(associated_item_ids))
7504
7504
  return result if isinstance(result, dict) else {"result": result}
7505
7505
 
7506
- return self.apps._run(profile, runner)
7506
+ return self.apps._run(profile, runner, tool_name="关联资源排序")
7507
7507
 
7508
7508
  def _update_view_associated_resources_config(
7509
7509
  self,
@@ -13246,7 +13246,7 @@ class AiBuilderFacade:
13246
13246
  params={"beingDraft": True},
13247
13247
  )
13248
13248
 
13249
- self.buttons._run(profile, runner)
13249
+ self.buttons._run(profile, runner, tool_name="自定义按钮删除校验")
13250
13250
  except (QingflowApiError, RuntimeError) as error:
13251
13251
  api_error = _coerce_api_error(error)
13252
13252
  if _delete_readback_is_not_found(api_error):
@@ -6,15 +6,20 @@ from typing import Any
6
6
 
7
7
  import anyio
8
8
  import uvicorn
9
+ from mcp.server.fastmcp import FastMCP
9
10
 
10
11
  from .backend_client import BackendClient
11
12
  from .cloud_auth import CloudAuthBroker
12
13
  from .cloud_context import current_cloud_context
13
14
  from .config import get_cloud_http_allowed_hosts, get_cloud_http_host, get_cloud_http_port
14
15
  from .errors import QingflowApiError, is_auth_like_error
16
+ from .server_app_builder import build_builder_server
15
17
  from .server_app_user import build_user_server
16
18
 
17
19
 
20
+ BUILDER_TOOL_PREFIX = "builder_"
21
+
22
+
18
23
  ASGIApp = Callable[
19
24
  [
20
25
  dict[str, Any],
@@ -94,6 +99,7 @@ class CloudMcpAuthApp:
94
99
 
95
100
  token = current_cloud_context.set(cloud_context)
96
101
  try:
102
+ scope = _ensure_streamable_http_accept(scope)
97
103
  await self._app(scope, receive, send)
98
104
  finally:
99
105
  current_cloud_context.reset(token)
@@ -160,16 +166,67 @@ class LazyCloudApp:
160
166
  send: Callable[[dict[str, Any]], Awaitable[None]],
161
167
  ) -> None:
162
168
  if self._app is None:
163
- self._app = build_cloud_user_app()
169
+ self._app = build_cloud_app()
164
170
  await self._app(scope, receive, send)
165
171
 
166
172
 
167
173
  def build_cloud_user_app() -> CloudMcpAuthApp:
168
- server = build_user_server()
174
+ return build_cloud_app()
175
+
176
+
177
+ def build_cloud_app() -> CloudMcpAuthApp:
178
+ server = build_combined_server()
169
179
  broker = CloudAuthBroker.from_config(backend=BackendClient())
170
180
  return CloudMcpAuthApp(server.streamable_http_app(), broker)
171
181
 
172
182
 
183
+ def build_combined_server() -> FastMCP:
184
+ server = build_user_server()
185
+ builder_server = build_builder_server()
186
+ server._mcp_server.name = "Qingflow Combined MCP"
187
+ _register_prefixed_builder_tools(server, builder_server)
188
+ _set_combined_instructions(server, builder_server)
189
+ return server
190
+
191
+
192
+ def _register_prefixed_builder_tools(target_server: FastMCP, builder_server: FastMCP) -> None:
193
+ for original_name, tool in builder_server._tool_manager._tools.items():
194
+ target_server._tool_manager.add_tool(
195
+ tool.fn,
196
+ name=f"{BUILDER_TOOL_PREFIX}{original_name}",
197
+ title=tool.title,
198
+ description=tool.description,
199
+ annotations=tool.annotations,
200
+ icons=tool.icons,
201
+ meta=tool.meta,
202
+ )
203
+
204
+
205
+ def _set_combined_instructions(user_server: FastMCP, builder_server: FastMCP) -> None:
206
+ user_instructions = user_server.instructions or ""
207
+ builder_instructions = builder_server.instructions or ""
208
+ user_server._mcp_server.instructions = f"""Use this combined Qingflow MCP server for both user operational workflows and builder workflows.
209
+
210
+ ## Tool Namespaces
211
+
212
+ User tools keep their original names, such as `app_get`, `record_list`, `task_list`, `portal_get`,
213
+ and `chart_get`.
214
+ Builder tools are exposed with the `{BUILDER_TOOL_PREFIX}` prefix. For example, Builder `app_get` is
215
+ `builder_app_get`, Builder `view_get` is `builder_view_get`, and Builder `builder_tool_contract` is
216
+ `builder_builder_tool_contract`.
217
+ When reading the Builder workflow instructions below, add `{BUILDER_TOOL_PREFIX}` to every Builder tool
218
+ name before calling it.
219
+
220
+ ## User Workflows
221
+
222
+ {user_instructions}
223
+
224
+ ## Builder Workflows
225
+
226
+ {builder_instructions}
227
+ """
228
+
229
+
173
230
  def _extract_host(scope: dict[str, Any]) -> str:
174
231
  for key, value in scope.get("headers", []):
175
232
  if key.lower() == b"host":
@@ -222,11 +279,40 @@ def _rewrite_host(scope: dict[str, Any], internal_host: str) -> dict[str, Any]:
222
279
  return rewritten
223
280
 
224
281
 
282
+ def _ensure_streamable_http_accept(scope: dict[str, Any]) -> dict[str, Any]:
283
+ existing_values: list[str] = []
284
+ headers_without_accept: list[tuple[bytes, bytes]] = []
285
+ for key, value in scope.get("headers", []):
286
+ if key.lower() == b"accept":
287
+ existing_values.append(value.decode("latin1"))
288
+ else:
289
+ headers_without_accept.append((key, value))
290
+
291
+ required = ["application/json", "text/event-stream"]
292
+ merged_values = [value.strip() for value in ",".join(existing_values).split(",") if value.strip()]
293
+ normalized_media_types = {
294
+ value.split(";", 1)[0].strip().lower()
295
+ for value in merged_values
296
+ }
297
+ if not merged_values or "*/*" in normalized_media_types:
298
+ merged_values = list(required)
299
+ else:
300
+ for media_type in required:
301
+ if media_type not in normalized_media_types:
302
+ merged_values.append(media_type)
303
+
304
+ rewritten = dict(scope)
305
+ rewritten["headers"] = headers_without_accept + [
306
+ (b"accept", ", ".join(merged_values).encode("latin1"))
307
+ ]
308
+ return rewritten
309
+
310
+
225
311
  app = LazyCloudApp()
226
312
 
227
313
 
228
314
  def main() -> None:
229
- uvicorn.run(build_cloud_user_app(), host=get_cloud_http_host(), port=get_cloud_http_port())
315
+ uvicorn.run(build_cloud_app(), host=get_cloud_http_host(), port=get_cloud_http_port())
230
316
 
231
317
 
232
318
  if __name__ == "__main__":