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

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 (44) 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 +376 -19
  7. package/src/qingflow_mcp/cli/commands/auth.py +14 -43
  8. package/src/qingflow_mcp/cli/commands/workspace.py +8 -5
  9. package/src/qingflow_mcp/cli/formatters.py +19 -22
  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 +4 -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/solution/compiler/form_compiler.py +2 -2
  19. package/src/qingflow_mcp/solution/executor.py +2 -2
  20. package/src/qingflow_mcp/tools/ai_builder_tools.py +117 -1
  21. package/src/qingflow_mcp/tools/app_tools.py +51 -1
  22. package/src/qingflow_mcp/tools/approval_tools.py +82 -1
  23. package/src/qingflow_mcp/tools/auth_tools.py +306 -288
  24. package/src/qingflow_mcp/tools/base.py +204 -4
  25. package/src/qingflow_mcp/tools/code_block_tools.py +21 -0
  26. package/src/qingflow_mcp/tools/custom_button_tools.py +24 -1
  27. package/src/qingflow_mcp/tools/directory_tools.py +28 -1
  28. package/src/qingflow_mcp/tools/feedback_tools.py +8 -0
  29. package/src/qingflow_mcp/tools/file_tools.py +25 -1
  30. package/src/qingflow_mcp/tools/import_tools.py +40 -1
  31. package/src/qingflow_mcp/tools/navigation_tools.py +34 -1
  32. package/src/qingflow_mcp/tools/package_tools.py +37 -1
  33. package/src/qingflow_mcp/tools/portal_tools.py +28 -1
  34. package/src/qingflow_mcp/tools/qingbi_report_tools.py +38 -1
  35. package/src/qingflow_mcp/tools/record_tools.py +255 -2
  36. package/src/qingflow_mcp/tools/repository_dev_tools.py +21 -2
  37. package/src/qingflow_mcp/tools/resource_read_tools.py +23 -1
  38. package/src/qingflow_mcp/tools/role_tools.py +19 -1
  39. package/src/qingflow_mcp/tools/solution_tools.py +56 -1
  40. package/src/qingflow_mcp/tools/task_context_tools.py +72 -1
  41. package/src/qingflow_mcp/tools/task_tools.py +49 -3
  42. package/src/qingflow_mcp/tools/view_tools.py +56 -1
  43. package/src/qingflow_mcp/tools/workflow_tools.py +65 -1
  44. package/src/qingflow_mcp/tools/workspace_tools.py +100 -217
@@ -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,9 +15,9 @@ 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")
18
+ get_parser = workspace_subparsers.add_parser("get", help="读取工作区详情")
19
+ get_parser.add_argument("--ws-id", type=int, default=0)
20
+ get_parser.set_defaults(handler=_handle_get, format_hint="workspace_get")
21
21
 
22
22
 
23
23
  def _handle_list(args: argparse.Namespace, context: CliContext) -> dict:
@@ -29,5 +29,8 @@ def _handle_list(args: argparse.Namespace, context: CliContext) -> dict:
29
29
  )
30
30
 
31
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)
32
+ def _handle_get(args: argparse.Namespace, context: CliContext) -> dict:
33
+ return context.workspace.workspace_get(
34
+ profile=args.profile,
35
+ ws_id=args.ws_id if int(args.ws_id or 0) > 0 else None,
36
+ )
@@ -38,8 +38,12 @@ def _format_whoami(result: dict[str, Any]) -> str:
38
38
  f"User: {result.get('nick_name') or '-'} ({result.get('email') or '-'})",
39
39
  f"UID: {result.get('uid')}",
40
40
  f"Workspace: {result.get('selected_ws_name') or '-'} ({result.get('selected_ws_id') or '-'})",
41
- f"QF Version: {result.get('qf_version') or '-'}",
41
+ f"Workspace QF Version: {result.get('qf_version') or '-'}",
42
42
  ]
43
+ request_route = result.get("request_route") if isinstance(result.get("request_route"), dict) else {}
44
+ route_qf_version = request_route.get("qf_version")
45
+ if route_qf_version and route_qf_version != result.get("qf_version"):
46
+ lines.append(f"Request Route QF Version: {route_qf_version}")
43
47
  lines.append(f"Permission Level: {result.get('permission_level') or '-'}")
44
48
  departments = result.get("departments") if isinstance(result.get("departments"), list) else []
45
49
  roles = result.get("roles") if isinstance(result.get("roles"), list) else []
@@ -82,6 +86,19 @@ def _format_workspace_list(result: dict[str, Any]) -> str:
82
86
  return _render_titled_table("Workspaces", ["ws_id", "name", "remark"], rows)
83
87
 
84
88
 
89
+ def _format_workspace_get(result: dict[str, Any]) -> str:
90
+ workspace = result.get("workspace") if isinstance(result.get("workspace"), dict) else {}
91
+ lines = [
92
+ f"Workspace: {workspace.get('workspaceName') or workspace.get('wsName') or '-'} ({workspace.get('wsId') or result.get('ws_id') or '-'})",
93
+ f"QF Version: {result.get('qf_version') or workspace.get('systemVersion') or '-'}",
94
+ f"Identity: {workspace.get('identity') or '-'}",
95
+ f"Auth: {workspace.get('auth') if workspace.get('auth') is not None else '-'}",
96
+ f"State: {workspace.get('state') if workspace.get('state') is not None else '-'}",
97
+ ]
98
+ _append_warnings(lines, result.get("warnings"))
99
+ return "\n".join(lines) + "\n"
100
+
101
+
85
102
  def _format_app_items(result: dict[str, Any]) -> str:
86
103
  items = result.get("items")
87
104
  if not isinstance(items, list):
@@ -132,26 +149,6 @@ def _format_app_get(result: dict[str, Any]) -> str:
132
149
  _append_warnings(lines, result.get("warnings"))
133
150
  return "\n".join(lines) + "\n"
134
151
 
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
152
  def _format_record_list(result: dict[str, Any]) -> str:
156
153
  data = result.get("data") if isinstance(result.get("data"), dict) else {}
157
154
  items = data.get("items") if isinstance(data.get("items"), list) else []
@@ -364,7 +361,7 @@ def _first_present(payload: dict[str, Any], *keys: str) -> Any:
364
361
  _FORMATTERS = {
365
362
  "auth_whoami": _format_whoami,
366
363
  "workspace_list": _format_workspace_list,
367
- "workspace_select": _format_workspace_select,
364
+ "workspace_get": _format_workspace_get,
368
365
  "app_list": _format_app_items,
369
366
  "app_search": _format_app_items,
370
367
  "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,11 @@ 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")),
37
+ PublicToolSpec(USER_DOMAIN, "workspace_get", ("workspace_get",), ("workspace", "get")),
39
38
  PublicToolSpec(USER_DOMAIN, "app_list", ("app_list",), ("app", "list"), cli_show_effective_context=True),
40
39
  PublicToolSpec(USER_DOMAIN, "app_search", ("app_search",), ("app", "search"), cli_show_effective_context=True),
41
40
  PublicToolSpec(USER_DOMAIN, "app_get", ("app_get",), ("app", "get"), cli_show_effective_context=True),
@@ -107,12 +106,11 @@ USER_PUBLIC_TOOL_SPECS: tuple[PublicToolSpec, ...] = (
107
106
 
108
107
 
109
108
  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),
109
+ PublicToolSpec(BUILDER_DOMAIN, "auth_use_credential", ("auth_use_credential",), ("builder", "auth", "use-credential"), cli_public=False),
112
110
  PublicToolSpec(BUILDER_DOMAIN, "auth_whoami", ("auth_whoami",), ("builder", "auth", "whoami"), cli_public=False),
113
111
  PublicToolSpec(BUILDER_DOMAIN, "auth_logout", ("auth_logout",), ("builder", "auth", "logout"), cli_public=False),
114
112
  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),
113
+ PublicToolSpec(BUILDER_DOMAIN, "workspace_get", ("workspace_get",), ("builder", "workspace", "get"), cli_public=False),
116
114
  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
115
  PublicToolSpec(BUILDER_DOMAIN, "feedback_submit", ("feedback_submit",), ("builder", "feedback", "submit"), has_contract=True),
118
116
  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)
@@ -29,6 +29,9 @@ class SessionProfile:
29
29
  base_url: str
30
30
  qf_version: str | None
31
31
  qf_version_source: str | None
32
+ token: str | None
33
+ login_token: str | None
34
+ credential: str | None
32
35
  uid: int
33
36
  email: str | None
34
37
  nick_name: str | None
@@ -45,6 +48,9 @@ class SessionProfile:
45
48
  base_url=value["base_url"],
46
49
  qf_version=value.get("qf_version"),
47
50
  qf_version_source=value.get("qf_version_source"),
51
+ token=value.get("token"),
52
+ login_token=value.get("login_token"),
53
+ credential=value.get("credential"),
48
54
  uid=value["uid"],
49
55
  email=value.get("email"),
50
56
  nick_name=value.get("nick_name"),
@@ -60,6 +66,7 @@ class SessionProfile:
60
66
  class BackendSession:
61
67
  token: str
62
68
  login_token: str | None
69
+ credential: str | None
63
70
  profile: str
64
71
  base_url: str
65
72
  qf_version: str | None
@@ -85,6 +92,7 @@ class SessionStore:
85
92
  qf_version_source: str | None = None,
86
93
  token: str,
87
94
  login_token: str | None,
95
+ credential: str | None = None,
88
96
  uid: int,
89
97
  email: str | None,
90
98
  nick_name: str | None,
@@ -99,14 +107,22 @@ class SessionStore:
99
107
  self._set_secret(self._login_token_key(profile), login_token)
100
108
  else:
101
109
  self._delete_secret(self._login_token_key(profile))
110
+ if credential:
111
+ self._set_secret(self._credential_key(profile), credential)
112
+ else:
113
+ self._delete_secret(self._credential_key(profile))
102
114
  else:
103
115
  self._delete_secret(self._token_key(profile))
104
116
  self._delete_secret(self._login_token_key(profile))
117
+ self._delete_secret(self._credential_key(profile))
105
118
  session_profile = SessionProfile(
106
119
  profile=profile,
107
120
  base_url=normalize_base_url(base_url) or base_url,
108
121
  qf_version=(str(qf_version).strip() or None) if qf_version is not None else None,
109
122
  qf_version_source=(str(qf_version_source).strip() or None) if qf_version_source is not None else None,
123
+ token=str(token).strip() or None,
124
+ login_token=(str(login_token).strip() or None) if login_token is not None else None,
125
+ credential=(str(credential).strip() or None) if credential is not None else None,
110
126
  uid=uid,
111
127
  email=email,
112
128
  nick_name=nick_name,
@@ -117,8 +133,9 @@ class SessionStore:
117
133
  updated_at=now,
118
134
  )
119
135
  self._memory_sessions[profile] = BackendSession(
120
- token=token,
121
- login_token=login_token,
136
+ token=session_profile.token or token,
137
+ login_token=session_profile.login_token,
138
+ credential=session_profile.credential,
122
139
  profile=profile,
123
140
  base_url=session_profile.base_url,
124
141
  qf_version=session_profile.qf_version,
@@ -146,14 +163,19 @@ class SessionStore:
146
163
  memory_session.qf_version = session_profile.qf_version
147
164
  memory_session.qf_version_source = session_profile.qf_version_source
148
165
  return memory_session
149
- if not session_profile or not session_profile.persisted:
166
+ if not session_profile:
150
167
  return None
151
- token = self._get_secret(self._token_key(profile))
168
+ token = self._get_secret(self._token_key(profile)) if session_profile.persisted else None
169
+ if not token:
170
+ token = session_profile.token
152
171
  if not token:
153
172
  return None
173
+ login_token = self._get_secret(self._login_token_key(profile)) if session_profile.persisted else None
174
+ credential = self._get_secret(self._credential_key(profile)) if session_profile.persisted else None
154
175
  backend_session = BackendSession(
155
176
  token=token,
156
- login_token=self._get_secret(self._login_token_key(profile)),
177
+ login_token=login_token or session_profile.login_token,
178
+ credential=credential or session_profile.credential,
157
179
  profile=profile,
158
180
  base_url=session_profile.base_url,
159
181
  qf_version=session_profile.qf_version,
@@ -234,6 +256,7 @@ class SessionStore:
234
256
  self._logged_out_profiles.discard(profile)
235
257
  self._delete_secret(self._token_key(profile))
236
258
  self._delete_secret(self._login_token_key(profile))
259
+ self._delete_secret(self._credential_key(profile))
237
260
  payload = self._load_profiles()
238
261
  profiles = payload.get("profiles", {})
239
262
  if profile in profiles:
@@ -249,6 +272,9 @@ class SessionStore:
249
272
  def _login_token_key(self, profile: str) -> str:
250
273
  return f"{profile}:login-token"
251
274
 
275
+ def _credential_key(self, profile: str) -> str:
276
+ return f"{profile}:credential"
277
+
252
278
  def _upsert_profile(self, profile: SessionProfile) -> None:
253
279
  payload = self._load_profiles()
254
280
  payload.setdefault("profiles", {})[profile.profile] = asdict(profile)
@@ -307,7 +307,7 @@ def build_reference_config(field: dict[str, Any], temp_id: int) -> dict[str, Any
307
307
  "queId": que_id,
308
308
  "queTitle": label,
309
309
  "queType": _normalize_reference_que_type(raw_type) or "2",
310
- "queAuth": 1,
310
+ "queAuth": 3,
311
311
  "ordinal": ordinal,
312
312
  "quoteId": temp_id,
313
313
  "_field_id": field_id,
@@ -320,7 +320,7 @@ def build_reference_config(field: dict[str, Any], temp_id: int) -> dict[str, Any
320
320
  auth_ques = []
321
321
  for ordinal, field_id in enumerate(auth_field_ids, start=1):
322
322
  que_id = auth_field_que_ids[ordinal - 1] if ordinal - 1 < len(auth_field_que_ids) else 0
323
- auth_ques.append({"queId": que_id, "queAuth": 1, "_field_id": field_id})
323
+ auth_ques.append({"queId": que_id, "queAuth": 3, "_field_id": field_id})
324
324
  return {
325
325
  "referAppKey": "__TARGET_APP_KEY__",
326
326
  "referQueId": display_field_que_id,
@@ -857,12 +857,12 @@ class SolutionExecutor:
857
857
  if refer_que_id is None:
858
858
  continue
859
859
  resolved["queId"] = refer_que_id
860
- resolved["queAuth"] = int(resolved.get("queAuth", 1))
860
+ resolved["queAuth"] = int(resolved.get("queAuth", 3))
861
861
  auth_ques.append(resolved)
862
862
  if not auth_ques:
863
863
  fallback_que_id = target_meta.get("by_field_id", {}).get(target_field_id)
864
864
  if fallback_que_id is not None:
865
- auth_ques.append({"queId": fallback_que_id, "queAuth": 1})
865
+ auth_ques.append({"queId": fallback_que_id, "queAuth": 3})
866
866
  reference_config["referAuthQues"] = auth_ques
867
867
  reference_config["fieldNameShow"] = bool(reference_config.get("fieldNameShow", True))
868
868
  fill_rules = []