@josephyan/qingflow-cli 0.2.0-beta.985 → 0.2.0-beta.986

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 +70 -11
  3. package/package.json +1 -1
  4. package/pyproject.toml +1 -1
  5. package/src/qingflow_mcp/__init__.py +1 -1
  6. package/src/qingflow_mcp/builder_facade/service.py +1 -1
  7. package/src/qingflow_mcp/cli/commands/auth.py +14 -43
  8. package/src/qingflow_mcp/cli/commands/workspace.py +0 -8
  9. package/src/qingflow_mcp/cli/formatters.py +0 -21
  10. package/src/qingflow_mcp/config.py +39 -0
  11. package/src/qingflow_mcp/errors.py +2 -2
  12. package/src/qingflow_mcp/public_surface.py +2 -6
  13. package/src/qingflow_mcp/response_trim.py +1 -8
  14. package/src/qingflow_mcp/server.py +1 -1
  15. package/src/qingflow_mcp/server_app_builder.py +4 -28
  16. package/src/qingflow_mcp/server_app_user.py +4 -28
  17. package/src/qingflow_mcp/session_store.py +31 -5
  18. package/src/qingflow_mcp/tools/ai_builder_tools.py +117 -1
  19. package/src/qingflow_mcp/tools/app_tools.py +51 -1
  20. package/src/qingflow_mcp/tools/approval_tools.py +82 -1
  21. package/src/qingflow_mcp/tools/auth_tools.py +258 -288
  22. package/src/qingflow_mcp/tools/base.py +204 -4
  23. package/src/qingflow_mcp/tools/code_block_tools.py +21 -0
  24. package/src/qingflow_mcp/tools/custom_button_tools.py +24 -1
  25. package/src/qingflow_mcp/tools/directory_tools.py +28 -1
  26. package/src/qingflow_mcp/tools/feedback_tools.py +8 -0
  27. package/src/qingflow_mcp/tools/file_tools.py +25 -1
  28. package/src/qingflow_mcp/tools/import_tools.py +40 -1
  29. package/src/qingflow_mcp/tools/navigation_tools.py +34 -1
  30. package/src/qingflow_mcp/tools/package_tools.py +37 -1
  31. package/src/qingflow_mcp/tools/portal_tools.py +28 -1
  32. package/src/qingflow_mcp/tools/qingbi_report_tools.py +38 -1
  33. package/src/qingflow_mcp/tools/record_tools.py +255 -2
  34. package/src/qingflow_mcp/tools/repository_dev_tools.py +21 -2
  35. package/src/qingflow_mcp/tools/resource_read_tools.py +23 -1
  36. package/src/qingflow_mcp/tools/role_tools.py +19 -1
  37. package/src/qingflow_mcp/tools/solution_tools.py +56 -1
  38. package/src/qingflow_mcp/tools/task_context_tools.py +72 -1
  39. package/src/qingflow_mcp/tools/task_tools.py +49 -3
  40. package/src/qingflow_mcp/tools/view_tools.py +56 -1
  41. package/src/qingflow_mcp/tools/workflow_tools.py +65 -1
  42. package/src/qingflow_mcp/tools/workspace_tools.py +14 -225
package/README.md CHANGED
@@ -3,13 +3,13 @@
3
3
  Install:
4
4
 
5
5
  ```bash
6
- npm install @josephyan/qingflow-cli@0.2.0-beta.985
6
+ npm install @josephyan/qingflow-cli@0.2.0-beta.986
7
7
  ```
8
8
 
9
9
  Run:
10
10
 
11
11
  ```bash
12
- npx -y -p @josephyan/qingflow-cli@0.2.0-beta.985 qingflow
12
+ npx -y -p @josephyan/qingflow-cli@0.2.0-beta.986 qingflow
13
13
  ```
14
14
 
15
15
  Environment:
@@ -6,6 +6,20 @@
6
6
  2. 记录/待办优先的 `qingflow-app-user-mcp`
7
7
  3. 精简 builder 的 `qingflow-app-builder-mcp`
8
8
 
9
+ ## 本地鉴权推荐方案
10
+
11
+ 本地模式现在推荐优先使用 `credential` 建立会话,而不是直接注入 `token`。
12
+
13
+ 推荐链路:
14
+
15
+ 1. createClaw 或其它本地宿主为当前实例保存 `credential`
16
+ 2. 本地 MCP 调用 `auth_use_credential`
17
+ 3. MCP 用该 `credential` 请求 apaas `/mcp/auth/context`
18
+ 4. 解析并保存 `token / wsId / qfVersion / uid`
19
+ 5. 业务工具直接使用这份上下文
20
+
21
+ `auth_use_credential` 是本地唯一鉴权主路径。
22
+
9
23
  ## npm 安装器适用场景
10
24
 
11
25
  适合这类本地 agent / gateway:
@@ -63,17 +77,17 @@ npm run pack:npm
63
77
  会生成:
64
78
 
65
79
  ```bash
66
- dist/npm/josephyan-qingflow-cli-<version>.tgz
67
- dist/npm/josephyan-qingflow-app-user-mcp-<version>.tgz
68
- dist/npm/josephyan-qingflow-app-builder-mcp-<version>.tgz
80
+ dist/npm/qingflow-tech-qingflow-cli-<version>.tgz
81
+ dist/npm/qingflow-tech-qingflow-app-user-mcp-<version>.tgz
82
+ dist/npm/qingflow-tech-qingflow-app-builder-mcp-<version>.tgz
69
83
  ```
70
84
 
71
85
  然后在目标机器安装:
72
86
 
73
87
  ```bash
74
- npm install /absolute/path/to/dist/npm/josephyan-qingflow-cli-<version>.tgz
75
- npm install /absolute/path/to/dist/npm/josephyan-qingflow-app-user-mcp-<version>.tgz
76
- npm install /absolute/path/to/dist/npm/josephyan-qingflow-app-builder-mcp-<version>.tgz
88
+ npm install /absolute/path/to/dist/npm/qingflow-tech-qingflow-cli-<version>.tgz
89
+ npm install /absolute/path/to/dist/npm/qingflow-tech-qingflow-app-user-mcp-<version>.tgz
90
+ npm install /absolute/path/to/dist/npm/qingflow-tech-qingflow-app-builder-mcp-<version>.tgz
77
91
  ```
78
92
 
79
93
  安装时会自动:
@@ -136,7 +150,10 @@ qingflow-app-builder-mcp
136
150
  ],
137
151
  "env": {
138
152
  "QINGFLOW_MCP_DEFAULT_BASE_URL": "https://qingflow.com/api",
139
- "QINGFLOW_MCP_HOME": "/absolute/path/to/.qingflow-mcp"
153
+ "QINGFLOW_MCP_HOME": "/absolute/path/to/.qingflow-mcp",
154
+ "QINGFLOW_MCP_CREDIT_METER_ENABLED": "true",
155
+ "QINGFLOW_MCP_CREDIT_APAAS_BASE_URL": "https://apaas.internal.example.com",
156
+ "QINGFLOW_MCP_CREDIT_APAAS_PATH": "/user/credit/usage"
140
157
  }
141
158
  }
142
159
  }
@@ -153,7 +170,10 @@ qingflow-app-builder-mcp
153
170
  "args": [],
154
171
  "env": {
155
172
  "QINGFLOW_MCP_DEFAULT_BASE_URL": "https://qingflow.com/api",
156
- "QINGFLOW_MCP_HOME": "/absolute/path/to/.qingflow-mcp"
173
+ "QINGFLOW_MCP_HOME": "/absolute/path/to/.qingflow-mcp",
174
+ "QINGFLOW_MCP_CREDIT_METER_ENABLED": "true",
175
+ "QINGFLOW_MCP_CREDIT_APAAS_BASE_URL": "https://apaas.internal.example.com",
176
+ "QINGFLOW_MCP_CREDIT_APAAS_PATH": "/user/credit/usage"
157
177
  }
158
178
  }
159
179
  }
@@ -170,7 +190,10 @@ qingflow-app-builder-mcp
170
190
  "args": [],
171
191
  "env": {
172
192
  "QINGFLOW_MCP_DEFAULT_BASE_URL": "https://qingflow.com/api",
173
- "QINGFLOW_MCP_HOME": "/absolute/path/to/.qingflow-mcp"
193
+ "QINGFLOW_MCP_HOME": "/absolute/path/to/.qingflow-mcp",
194
+ "QINGFLOW_MCP_CREDIT_METER_ENABLED": "true",
195
+ "QINGFLOW_MCP_CREDIT_APAAS_BASE_URL": "https://apaas.internal.example.com",
196
+ "QINGFLOW_MCP_CREDIT_APAAS_PATH": "/user/credit/usage"
174
197
  }
175
198
  }
176
199
  }
@@ -191,7 +214,10 @@ qingflow-app-builder-mcp
191
214
  "@josephyan/qingflow-app-user-mcp"
192
215
  ],
193
216
  "env": {
194
- "QINGFLOW_MCP_DEFAULT_BASE_URL": "https://qingflow.com/api"
217
+ "QINGFLOW_MCP_DEFAULT_BASE_URL": "https://qingflow.com/api",
218
+ "QINGFLOW_MCP_CREDIT_METER_ENABLED": "true",
219
+ "QINGFLOW_MCP_CREDIT_APAAS_BASE_URL": "https://apaas.internal.example.com",
220
+ "QINGFLOW_MCP_CREDIT_APAAS_PATH": "/user/credit/usage"
195
221
  }
196
222
  },
197
223
  "qingflow-builder": {
@@ -201,7 +227,10 @@ qingflow-app-builder-mcp
201
227
  "@josephyan/qingflow-app-builder-mcp"
202
228
  ],
203
229
  "env": {
204
- "QINGFLOW_MCP_DEFAULT_BASE_URL": "https://qingflow.com/api"
230
+ "QINGFLOW_MCP_DEFAULT_BASE_URL": "https://qingflow.com/api",
231
+ "QINGFLOW_MCP_CREDIT_METER_ENABLED": "true",
232
+ "QINGFLOW_MCP_CREDIT_APAAS_BASE_URL": "https://apaas.internal.example.com",
233
+ "QINGFLOW_MCP_CREDIT_APAAS_PATH": "/user/credit/usage"
205
234
  }
206
235
  }
207
236
  }
@@ -212,6 +241,7 @@ qingflow-app-builder-mcp
212
241
  - 源码目录 `npm install` 不会把命令加到全局 PATH;这种模式请用 `node ./npm/bin/qingflow.mjs`、`node ./npm/bin/qingflow-app-user-mcp.mjs` 或 `node ./npm/bin/qingflow-app-builder-mcp.mjs`
213
242
  - `npx` 方式适合临时安装或容器化本地 agent
214
243
  - 全局安装方式更适合长期固定使用的本机开发环境
244
+ - 计费接口使用当前登录会话的 `token` 与 `wsId` 请求头,可通过 `QINGFLOW_MCP_CREDIT_APAAS_BASE_URL/PATH` 覆盖调用记录接口地址
215
245
 
216
246
  ## 排障
217
247
 
@@ -242,3 +272,32 @@ npm install
242
272
  4. 再启动 MCP 客户端
243
273
 
244
274
  现在 stdio MCP 入口会拒绝在启动瞬间“边启动边重建 Python 运行时”,因为安装日志一旦写进 stdout,就会破坏 MCP 握手并表现成 `Transport closed`。如果运行时缺失或版本不一致,入口会直接报错并提示重装,而不是静默自修复。
275
+
276
+ ## createClaw 本地接入示例
277
+
278
+ 如果 createClaw 已经为当前本地实例保存了 `credential`,推荐在首次建链时调用:
279
+
280
+ ```bash
281
+ qingflow auth use-credential \
282
+ --base-url https://qingflow.com/api \
283
+ --credential-stdin
284
+ ```
285
+
286
+ 然后把 `credential` 写到 stdin。
287
+
288
+ 等价 MCP 工具调用参数:
289
+
290
+ ```json
291
+ {
292
+ "profile": "default",
293
+ "base_url": "https://qingflow.com/api",
294
+ "credential": "1602853_277941",
295
+ "persist": false
296
+ }
297
+ ```
298
+
299
+ 说明:
300
+
301
+ - 本地会把解析后的 `token` 和原始 `credential` 写入 profile 文件,用于后续 CLI 命令恢复会话
302
+ - `persist=true` 时,本地还会优先把解析后的 `token` 和原始 `credential` 同步写入系统 keychain
303
+ - 当前工作区以 `/mcp/auth/context` 返回的 `wsId` 为准,不再通过本地 MCP 显式切换
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@josephyan/qingflow-cli",
3
- "version": "0.2.0-beta.985",
3
+ "version": "0.2.0-beta.986",
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 = "0.2.0b985"
7
+ version = "0.2.0b986"
8
8
  description = "User-authenticated MCP server for Qingflow"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -5,7 +5,7 @@ from pathlib import Path
5
5
 
6
6
  __all__ = ["__version__"]
7
7
 
8
- _FALLBACK_VERSION = "0.2.0b985"
8
+ _FALLBACK_VERSION = "0.2.0b986"
9
9
 
10
10
 
11
11
  def _resolve_local_pyproject_version() -> str | None:
@@ -7708,7 +7708,7 @@ class AiBuilderFacade:
7708
7708
  "request_route": request_route,
7709
7709
  },
7710
7710
  suggested_next_call=(
7711
- {"tool_name": "workspace_select", "arguments": {"profile": profile, "ws_id": request_route.get("ws_id")}}
7711
+ {"tool_name": "auth_use_credential", "arguments": {"profile": profile}}
7712
7712
  if request_route.get("ws_id")
7713
7713
  else None
7714
7714
  ),
@@ -1,10 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import argparse
4
- import getpass
5
- import sys
6
4
 
7
- from ...errors import QingflowApiError
8
5
  from ..context import CliContext
9
6
  from .common import read_secret_arg
10
7
 
@@ -13,23 +10,13 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
13
10
  parser = subparsers.add_parser("auth", help="认证与会话")
14
11
  auth_subparsers = parser.add_subparsers(dest="auth_command", required=True)
15
12
 
16
- login = auth_subparsers.add_parser("login", help="邮箱密码登录")
17
- login.add_argument("--base-url")
18
- login.add_argument("--qf-version")
19
- login.add_argument("--email", required=True)
20
- login.add_argument("--password")
21
- login.add_argument("--password-stdin", action="store_true")
22
- login.add_argument("--persist", action=argparse.BooleanOptionalAction, default=True)
23
- login.set_defaults(handler=_handle_login, format_hint="auth_whoami")
24
-
25
- use_token = auth_subparsers.add_parser("use-token", help="直接注入 token")
26
- use_token.add_argument("--base-url")
27
- use_token.add_argument("--qf-version")
28
- use_token.add_argument("--token")
29
- use_token.add_argument("--token-stdin", action="store_true")
30
- use_token.add_argument("--ws-id", type=int)
31
- use_token.add_argument("--persist", action=argparse.BooleanOptionalAction, default=False)
32
- use_token.set_defaults(handler=_handle_use_token, format_hint="auth_whoami")
13
+ use_credential = auth_subparsers.add_parser("use-credential", help="直接注入 credential")
14
+ use_credential.add_argument("--base-url")
15
+ use_credential.add_argument("--qf-version")
16
+ use_credential.add_argument("--credential")
17
+ use_credential.add_argument("--credential-stdin", action="store_true")
18
+ use_credential.add_argument("--persist", action=argparse.BooleanOptionalAction, default=False)
19
+ use_credential.set_defaults(handler=_handle_use_credential, format_hint="auth_whoami")
33
20
 
34
21
  whoami = auth_subparsers.add_parser("whoami", help="查看当前登录态")
35
22
  whoami.set_defaults(handler=_handle_whoami, format_hint="auth_whoami")
@@ -39,33 +26,17 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
39
26
  logout.set_defaults(handler=_handle_logout, format_hint="")
40
27
 
41
28
 
42
- def _handle_login(args: argparse.Namespace, context: CliContext) -> dict:
43
- password = args.password
44
- if args.password_stdin:
45
- password = read_secret_arg(args.password, stdin_enabled=True, label="password")
46
- elif not password:
47
- if sys.stdin.isatty():
48
- password = getpass.getpass("Password: ")
49
- else:
50
- raise QingflowApiError.config_error("password is required; use --password or --password-stdin")
51
- return context.auth.auth_login(
52
- profile=args.profile,
53
- base_url=args.base_url,
54
- qf_version=args.qf_version,
55
- email=args.email,
56
- password=password,
57
- persist=bool(args.persist),
29
+ def _handle_use_credential(args: argparse.Namespace, context: CliContext) -> dict:
30
+ credential = (
31
+ read_secret_arg(args.credential, stdin_enabled=bool(args.credential_stdin), label="credential")
32
+ if args.credential or bool(args.credential_stdin)
33
+ else ""
58
34
  )
59
-
60
-
61
- def _handle_use_token(args: argparse.Namespace, context: CliContext) -> dict:
62
- token = read_secret_arg(args.token, stdin_enabled=bool(args.token_stdin), label="token")
63
- return context.auth.auth_use_token(
35
+ return context.auth.auth_use_credential(
64
36
  profile=args.profile,
65
37
  base_url=args.base_url,
66
38
  qf_version=args.qf_version,
67
- token=token,
68
- ws_id=args.ws_id,
39
+ credential=credential,
69
40
  persist=bool(args.persist),
70
41
  )
71
42
 
@@ -15,10 +15,6 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
15
15
  list_parser.add_argument("--include-external", action="store_true")
16
16
  list_parser.set_defaults(handler=_handle_list, format_hint="workspace_list")
17
17
 
18
- select = workspace_subparsers.add_parser("select", help="切换工作区")
19
- select.add_argument("--ws-id", type=int, required=True)
20
- select.set_defaults(handler=_handle_select, format_hint="workspace_select")
21
-
22
18
 
23
19
  def _handle_list(args: argparse.Namespace, context: CliContext) -> dict:
24
20
  return context.workspace.workspace_list(
@@ -27,7 +23,3 @@ def _handle_list(args: argparse.Namespace, context: CliContext) -> dict:
27
23
  page_size=args.page_size,
28
24
  include_external=bool(args.include_external),
29
25
  )
30
-
31
-
32
- def _handle_select(args: argparse.Namespace, context: CliContext) -> dict:
33
- return context.workspace.workspace_select(profile=args.profile, ws_id=args.ws_id)
@@ -132,26 +132,6 @@ def _format_app_get(result: dict[str, Any]) -> str:
132
132
  _append_warnings(lines, result.get("warnings"))
133
133
  return "\n".join(lines) + "\n"
134
134
 
135
-
136
- def _format_workspace_select(result: dict[str, Any]) -> str:
137
- lines = [
138
- f"Workspace: {result.get('selected_ws_name') or '-'} ({result.get('selected_ws_id') or '-'})",
139
- f"QF Version: {result.get('qf_version') or '-'}",
140
- ]
141
- workspace_version = result.get("workspace_version") if isinstance(result.get("workspace_version"), dict) else {}
142
- if workspace_version:
143
- lines.append(
144
- "Workspace Version: "
145
- f"{workspace_version.get('display_name') or '-'} "
146
- f"({workspace_version.get('level_name') or workspace_version.get('level_code') or '-'})"
147
- )
148
- if workspace_version.get("being_trial") is not None:
149
- lines.append(f"Trial: {workspace_version.get('being_trial')}")
150
- if workspace_version.get("expire_date") is not None:
151
- lines.append(f"Expire Date: {workspace_version.get('expire_date')}")
152
- return "\n".join(lines) + "\n"
153
-
154
-
155
135
  def _format_record_list(result: dict[str, Any]) -> str:
156
136
  data = result.get("data") if isinstance(result.get("data"), dict) else {}
157
137
  items = data.get("items") if isinstance(data.get("items"), list) else []
@@ -364,7 +344,6 @@ def _first_present(payload: dict[str, Any], *keys: str) -> Any:
364
344
  _FORMATTERS = {
365
345
  "auth_whoami": _format_whoami,
366
346
  "workspace_list": _format_workspace_list,
367
- "workspace_select": _format_workspace_select,
368
347
  "app_list": _format_app_items,
369
348
  "app_search": _format_app_items,
370
349
  "app_get": _format_app_get,
@@ -21,6 +21,8 @@ DEFAULT_REPOSITORY_PROD_BRANCH = "prod"
21
21
  DEFAULT_REPOSITORY_AUTHOR_NAME = "qingflow-mcp"
22
22
  DEFAULT_REPOSITORY_AUTHOR_EMAIL = "qingflow-mcp@local.invalid"
23
23
  DEFAULT_REPOSITORY_INTERNAL_SHARE_TOKEN_KEY = "tokenKey"
24
+ DEFAULT_CREDIT_USAGE_RECORD_PATH = "/user/credit/usage"
25
+ DEFAULT_MCPORTER_CONFIG_PATH = "~/.openclaw/workspace/config/mcporter.json"
24
26
 
25
27
 
26
28
  def get_mcp_home() -> Path:
@@ -32,6 +34,13 @@ def get_profiles_path() -> Path:
32
34
  return get_mcp_home() / "profiles.json"
33
35
 
34
36
 
37
+ def get_mcporter_config_path() -> Path:
38
+ custom_path = os.getenv("QINGFLOW_MCP_MCPORTER_CONFIG_PATH") or os.getenv(
39
+ "QINGFLOW_MCP_AUTH_CONFIG_PATH"
40
+ )
41
+ return Path(custom_path).expanduser() if custom_path else Path(DEFAULT_MCPORTER_CONFIG_PATH)
42
+
43
+
35
44
  def get_repository_metadata_dir() -> Path:
36
45
  return get_mcp_home() / "repository-metadata"
37
46
 
@@ -208,6 +217,36 @@ def get_log_level() -> str:
208
217
  )
209
218
 
210
219
 
220
+ def get_credit_meter_enabled() -> bool:
221
+ value = get_config_value(
222
+ "credit_meter.enabled",
223
+ env_var="QINGFLOW_MCP_CREDIT_METER_ENABLED",
224
+ default="true",
225
+ )
226
+ normalized = str(value or "").strip().lower()
227
+ return normalized in {"1", "true", "yes", "on"}
228
+
229
+
230
+ def get_credit_usage_base_url() -> str | None:
231
+ value = get_config_value(
232
+ "credit_meter.apaas.base_url",
233
+ env_var="QINGFLOW_MCP_CREDIT_APAAS_BASE_URL",
234
+ default=None,
235
+ )
236
+ normalized = normalize_base_url(value)
237
+ return normalized or None
238
+
239
+
240
+ def get_credit_usage_path() -> str:
241
+ value = get_config_value(
242
+ "credit_meter.apaas.path",
243
+ env_var="QINGFLOW_MCP_CREDIT_APAAS_PATH",
244
+ default=DEFAULT_CREDIT_USAGE_RECORD_PATH,
245
+ )
246
+ normalized = str(value or "").strip()
247
+ return normalized or DEFAULT_CREDIT_USAGE_RECORD_PATH
248
+
249
+
211
250
  def get_repository_default_group() -> str | None:
212
251
  value = get_config_value(
213
252
  "repository.default_group",
@@ -43,14 +43,14 @@ class QingflowApiError(Exception):
43
43
  def auth_required(cls, profile: str) -> "QingflowApiError":
44
44
  return cls(
45
45
  category="auth",
46
- message=f"Profile '{profile}' is not logged in. Run auth_login first.",
46
+ message=f"Profile '{profile}' is not logged in. Run auth_use_credential first.",
47
47
  )
48
48
 
49
49
  @classmethod
50
50
  def workspace_not_selected(cls, profile: str) -> "QingflowApiError":
51
51
  return cls(
52
52
  category="workspace",
53
- message=f"WORKSPACE_NOT_SELECTED: profile '{profile}' has no selected workspace. Run workspace_select first.",
53
+ message=f"WORKSPACE_NOT_SELECTED: profile '{profile}' has no workspace from auth context. Re-run auth_use_credential.",
54
54
  )
55
55
 
56
56
  @classmethod
@@ -30,12 +30,10 @@ def tool_key(domain: str, tool_name: str) -> str:
30
30
 
31
31
 
32
32
  USER_PUBLIC_TOOL_SPECS: tuple[PublicToolSpec, ...] = (
33
- PublicToolSpec(USER_DOMAIN, "auth_login", ("auth_login",), ("auth", "login")),
34
- PublicToolSpec(USER_DOMAIN, "auth_use_token", ("auth_use_token",), ("auth", "use-token")),
33
+ PublicToolSpec(USER_DOMAIN, "auth_use_credential", ("auth_use_credential",), ("auth", "use-credential")),
35
34
  PublicToolSpec(USER_DOMAIN, "auth_whoami", ("auth_whoami",), ("auth", "whoami")),
36
35
  PublicToolSpec(USER_DOMAIN, "auth_logout", ("auth_logout",), ("auth", "logout")),
37
36
  PublicToolSpec(USER_DOMAIN, "workspace_list", ("workspace_list",), ("workspace", "list")),
38
- PublicToolSpec(USER_DOMAIN, "workspace_select", ("workspace_select",), ("workspace", "select")),
39
37
  PublicToolSpec(USER_DOMAIN, "app_list", ("app_list",), ("app", "list"), cli_show_effective_context=True),
40
38
  PublicToolSpec(USER_DOMAIN, "app_search", ("app_search",), ("app", "search"), cli_show_effective_context=True),
41
39
  PublicToolSpec(USER_DOMAIN, "app_get", ("app_get",), ("app", "get"), cli_show_effective_context=True),
@@ -107,12 +105,10 @@ USER_PUBLIC_TOOL_SPECS: tuple[PublicToolSpec, ...] = (
107
105
 
108
106
 
109
107
  BUILDER_PUBLIC_TOOL_SPECS: tuple[PublicToolSpec, ...] = (
110
- PublicToolSpec(BUILDER_DOMAIN, "auth_login", ("auth_login",), ("builder", "auth", "login"), cli_public=False),
111
- PublicToolSpec(BUILDER_DOMAIN, "auth_use_token", ("auth_use_token",), ("builder", "auth", "use-token"), cli_public=False),
108
+ PublicToolSpec(BUILDER_DOMAIN, "auth_use_credential", ("auth_use_credential",), ("builder", "auth", "use-credential"), cli_public=False),
112
109
  PublicToolSpec(BUILDER_DOMAIN, "auth_whoami", ("auth_whoami",), ("builder", "auth", "whoami"), cli_public=False),
113
110
  PublicToolSpec(BUILDER_DOMAIN, "auth_logout", ("auth_logout",), ("builder", "auth", "logout"), cli_public=False),
114
111
  PublicToolSpec(BUILDER_DOMAIN, "workspace_list", ("workspace_list",), ("builder", "workspace", "list"), cli_public=False),
115
- PublicToolSpec(BUILDER_DOMAIN, "workspace_select", ("workspace_select",), ("builder", "workspace", "select"), cli_public=False),
116
112
  PublicToolSpec(BUILDER_DOMAIN, "file_upload_local", ("file_upload_local",), ("builder", "file", "upload-local"), has_contract=True, cli_show_effective_context=True, cli_context_write=True),
117
113
  PublicToolSpec(BUILDER_DOMAIN, "feedback_submit", ("feedback_submit",), ("builder", "feedback", "submit"), has_contract=True),
118
114
  PublicToolSpec(BUILDER_DOMAIN, "builder_tool_contract", ("builder_tool_contract",), ("builder", "contract"), has_contract=False),
@@ -263,12 +263,6 @@ def _trim_workspace_list(payload: JSONObject) -> None:
263
263
  _trim_item_list(page, "list", allowed=("wsId", "workspaceName", "remark"))
264
264
 
265
265
 
266
- def _trim_workspace_select(payload: JSONObject) -> None:
267
- workspace = payload.get("workspace")
268
- if isinstance(workspace, dict):
269
- payload["workspace"] = _pick(workspace, ("wsId", "workspaceName"))
270
-
271
-
272
266
  def _trim_app_search_like(payload: JSONObject) -> None:
273
267
  payload.pop("apps", None)
274
268
  _trim_item_list(payload, "items", allowed=("app_key", "app_name", "package_name"))
@@ -731,10 +725,9 @@ def _register_policy(domains: tuple[str, ...], names: tuple[str, ...], transform
731
725
  SUCCESS_POLICY_BY_TOOL[tool_key(domain, name)] = transform
732
726
 
733
727
 
734
- _register_policy((USER_DOMAIN, BUILDER_DOMAIN), ("auth_login", "auth_use_token", "auth_whoami"), _trim_auth_payload)
728
+ _register_policy((USER_DOMAIN, BUILDER_DOMAIN), ("auth_use_credential", "auth_whoami"), _trim_auth_payload)
735
729
  _register_policy((USER_DOMAIN, BUILDER_DOMAIN), ("auth_logout",), _trim_auth_logout)
736
730
  _register_policy((USER_DOMAIN, BUILDER_DOMAIN), ("workspace_list",), _trim_workspace_list)
737
- _register_policy((USER_DOMAIN, BUILDER_DOMAIN), ("workspace_select",), _trim_workspace_select)
738
731
  _register_policy((USER_DOMAIN,), ("app_list", "app_search"), _trim_app_search_like)
739
732
  _register_policy((USER_DOMAIN, BUILDER_DOMAIN), ("app_get",), _trim_app_get)
740
733
  _register_policy((BUILDER_DOMAIN,), ("app_repair_code_blocks",), _trim_builder_list_like)
@@ -34,7 +34,7 @@ def build_server() -> FastMCP:
34
34
 
35
35
  ## Authentication
36
36
 
37
- Use `auth_login` first, then `workspace_list` and `workspace_select`.
37
+ Use `auth_use_credential` first when a local host such as createClaw can provide a credential. Treat the returned `wsId` and `qfVersion` as authoritative for the local session.
38
38
  All resource tools operate with the logged-in user's Qingflow permissions.
39
39
 
40
40
  ## Shared Helper
@@ -56,38 +56,18 @@ def build_builder_server() -> FastMCP:
56
56
  feedback = FeedbackTools(backend, mcp_side="App Builder MCP")
57
57
 
58
58
  @server.tool()
59
- def auth_login(
59
+ def auth_use_credential(
60
60
  profile: str = DEFAULT_PROFILE,
61
61
  base_url: str | None = None,
62
62
  qf_version: str | None = None,
63
- email: str = "",
64
- password: str = "",
65
- persist: bool = True,
66
- ) -> dict:
67
- return auth.auth_login(
68
- profile=profile,
69
- base_url=base_url,
70
- qf_version=qf_version,
71
- email=email,
72
- password=password,
73
- persist=persist,
74
- )
75
-
76
- @server.tool()
77
- def auth_use_token(
78
- profile: str = DEFAULT_PROFILE,
79
- base_url: str | None = None,
80
- qf_version: str | None = None,
81
- token: str = "",
82
- ws_id: int | None = None,
63
+ credential: str = "",
83
64
  persist: bool = False,
84
65
  ) -> dict:
85
- return auth.auth_use_token(
66
+ return auth.auth_use_credential(
86
67
  profile=profile,
87
68
  base_url=base_url,
88
69
  qf_version=qf_version,
89
- token=token,
90
- ws_id=ws_id,
70
+ credential=credential,
91
71
  persist=persist,
92
72
  )
93
73
 
@@ -113,10 +93,6 @@ def build_builder_server() -> FastMCP:
113
93
  include_external=include_external,
114
94
  )
115
95
 
116
- @server.tool()
117
- def workspace_select(profile: str = DEFAULT_PROFILE, ws_id: int = 0) -> dict:
118
- return workspace.workspace_select(profile=profile, ws_id=ws_id)
119
-
120
96
  @server.tool()
121
97
  def file_upload_local(
122
98
  profile: str = DEFAULT_PROFILE,
@@ -191,38 +191,18 @@ If the current MCP capability is unsupported, the workflow is awkward, or the us
191
191
  directory_tools = wrap_trimmed_methods(DirectoryTools(sessions, backend), USER_SERVER_METHOD_MAP)
192
192
 
193
193
  @server.tool()
194
- def auth_login(
194
+ def auth_use_credential(
195
195
  profile: str = DEFAULT_PROFILE,
196
196
  base_url: str | None = None,
197
197
  qf_version: str | None = None,
198
- email: str = "",
199
- password: str = "",
200
- persist: bool = True,
201
- ) -> dict:
202
- return auth.auth_login(
203
- profile=profile,
204
- base_url=base_url,
205
- qf_version=qf_version,
206
- email=email,
207
- password=password,
208
- persist=persist,
209
- )
210
-
211
- @server.tool()
212
- def auth_use_token(
213
- profile: str = DEFAULT_PROFILE,
214
- base_url: str | None = None,
215
- qf_version: str | None = None,
216
- token: str = "",
217
- ws_id: int | None = None,
198
+ credential: str = "",
218
199
  persist: bool = False,
219
200
  ) -> dict:
220
- return auth.auth_use_token(
201
+ return auth.auth_use_credential(
221
202
  profile=profile,
222
203
  base_url=base_url,
223
204
  qf_version=qf_version,
224
- token=token,
225
- ws_id=ws_id,
205
+ credential=credential,
226
206
  persist=persist,
227
207
  )
228
208
 
@@ -248,10 +228,6 @@ If the current MCP capability is unsupported, the workflow is awkward, or the us
248
228
  include_external=include_external,
249
229
  )
250
230
 
251
- @server.tool()
252
- def workspace_select(profile: str = DEFAULT_PROFILE, ws_id: int = 0) -> dict:
253
- return workspace.workspace_select(profile=profile, ws_id=ws_id)
254
-
255
231
  @server.tool()
256
232
  def app_list(profile: str = DEFAULT_PROFILE) -> dict:
257
233
  return apps.app_list(profile=profile)