@josephyan/qingflow-app-builder-mcp 0.2.0-beta.988 → 0.2.0-beta.990

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 CHANGED
@@ -3,13 +3,13 @@
3
3
  Install:
4
4
 
5
5
  ```bash
6
- npm install @josephyan/qingflow-app-builder-mcp@0.2.0-beta.988
6
+ npm install @josephyan/qingflow-app-builder-mcp@0.2.0-beta.990
7
7
  ```
8
8
 
9
9
  Run:
10
10
 
11
11
  ```bash
12
- npx -y -p @josephyan/qingflow-app-builder-mcp@0.2.0-beta.988 qingflow-app-builder-mcp
12
+ npx -y -p @josephyan/qingflow-app-builder-mcp@0.2.0-beta.990 qingflow-app-builder-mcp
13
13
  ```
14
14
 
15
15
  Environment:
@@ -20,6 +20,13 @@
20
20
 
21
21
  `auth_use_credential` 是本地唯一鉴权主路径。
22
22
 
23
+ 补充说明:
24
+
25
+ - 对 stdio MCP 来说,主路径仍然只有 `auth_use_credential`。
26
+ - 如果你是在终端里直接使用 `qingflow` CLI,可以额外使用 `qingflow auth login` 作为“人类登录”入口;默认会提示轻流邮箱和隐藏密码,拿到 `token` 后建立本地 CLI 会话。
27
+ - 如果需要浏览器 Authorization Code + PKCE 或 Device Flow,请显式使用 `qingflow auth login --browser` 或 `qingflow auth login --device`;这条 OAuth 路径最终拿到 `credential` 后再复用 `auth_use_credential` 建会话。
28
+ - 也就是说,这次新增的是 CLI 的登录入口,不是给 MCP 增加第二套会话模型。
29
+
23
30
  ## npm 安装器适用场景
24
31
 
25
32
  适合这类本地 agent / gateway:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@josephyan/qingflow-app-builder-mcp",
3
- "version": "0.2.0-beta.988",
3
+ "version": "0.2.0-beta.990",
4
4
  "description": "Builder MCP for Qingflow app/package/system design and staged solution workflows.",
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.0b988"
7
+ version = "0.2.0b990"
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.0b988"
8
+ _FALLBACK_VERSION = "0.2.0b990"
9
9
 
10
10
 
11
11
  def _resolve_local_pyproject_version() -> str | None:
@@ -1,8 +1,13 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import argparse
4
+ import getpass
5
+ import sys
4
6
 
7
+ from ...errors import QingflowApiError
5
8
  from ..context import CliContext
9
+ from ..oauth_login import login_with_cli_oauth
10
+ from ..qingflow_login import login_with_qingflow_password
6
11
  from .common import read_secret_arg
7
12
 
8
13
 
@@ -10,6 +15,23 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
10
15
  parser = subparsers.add_parser("auth", help="认证与会话")
11
16
  auth_subparsers = parser.add_subparsers(dest="auth_command", required=True)
12
17
 
18
+ login = auth_subparsers.add_parser("login", help="登录 CLI:默认轻流账号密码交互;--browser 使用 OAuth")
19
+ login.add_argument("--base-url")
20
+ login.add_argument("--qf-version")
21
+ login.add_argument("--client-id")
22
+ login.add_argument("--authorization-endpoint")
23
+ login.add_argument("--token-endpoint")
24
+ login.add_argument("--device-authorization-endpoint")
25
+ login.add_argument("--scope")
26
+ login.add_argument("--credential-field")
27
+ login.add_argument("--browser", action="store_true", help="使用浏览器 OAuth 登录;无浏览器时可配合 --device")
28
+ login.add_argument("--device", action="store_true", help="使用 OAuth Device Flow(隐含 --browser)")
29
+ login.add_argument("--email", help="轻流账号邮箱;不传时在交互终端提示输入")
30
+ login.add_argument("--password", help="轻流账号密码;建议仅用于本地调试,脚本请优先使用 --password-stdin")
31
+ login.add_argument("--password-stdin", action="store_true", help="从标准输入读取轻流账号密码")
32
+ login.add_argument("--persist", action=argparse.BooleanOptionalAction, default=True)
33
+ login.set_defaults(handler=_handle_login, format_hint="auth_whoami")
34
+
13
35
  use_credential = auth_subparsers.add_parser("use-credential", help="直接注入 credential")
14
36
  use_credential.add_argument("--base-url")
15
37
  use_credential.add_argument("--qf-version")
@@ -26,6 +48,112 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
26
48
  logout.set_defaults(handler=_handle_logout, format_hint="")
27
49
 
28
50
 
51
+ def _handle_login(args: argparse.Namespace, context: CliContext) -> dict:
52
+ login_summary: dict[str, str | None]
53
+ if _should_use_browser_login(args):
54
+ if _has_account_login_inputs(args):
55
+ raise QingflowApiError.config_error(
56
+ "Choose either account/password login or --browser OAuth login; do not combine both."
57
+ )
58
+ oauth_result = login_with_cli_oauth(
59
+ base_url=args.base_url,
60
+ client_id=args.client_id,
61
+ authorization_endpoint=args.authorization_endpoint,
62
+ token_endpoint=args.token_endpoint,
63
+ device_authorization_endpoint=args.device_authorization_endpoint,
64
+ scope=args.scope,
65
+ credential_field=args.credential_field,
66
+ force_device=bool(args.device),
67
+ )
68
+ result = context.auth.auth_use_credential(
69
+ profile=args.profile,
70
+ base_url=args.base_url,
71
+ qf_version=args.qf_version,
72
+ credential=oauth_result.credential,
73
+ persist=bool(args.persist),
74
+ )
75
+ login_summary = {
76
+ "flow": oauth_result.flow,
77
+ "authorize_url": oauth_result.authorize_url,
78
+ "verification_uri": oauth_result.verification_uri,
79
+ "user_code": oauth_result.user_code,
80
+ }
81
+ else:
82
+ email = _resolve_login_email(args)
83
+ password = _resolve_login_password(args)
84
+ login_result = login_with_qingflow_password(
85
+ base_url=args.base_url,
86
+ email=email,
87
+ password=password,
88
+ )
89
+ result = context.auth.auth_use_token(
90
+ profile=args.profile,
91
+ base_url=args.base_url,
92
+ qf_version=args.qf_version,
93
+ token=login_result.token,
94
+ login_token=login_result.login_token,
95
+ user_info=login_result.user_info,
96
+ persist=bool(args.persist),
97
+ )
98
+ login_summary = {
99
+ "flow": login_result.flow,
100
+ "authorize_url": None,
101
+ "verification_uri": None,
102
+ "user_code": None,
103
+ }
104
+ warnings = list(result.get("warnings") or []) if isinstance(result, dict) else []
105
+ if isinstance(result, dict):
106
+ result["cli_auth"] = {key: value for key, value in login_summary.items() if value}
107
+ if warnings:
108
+ result["warnings"] = warnings
109
+ return result
110
+
111
+
112
+ def _should_use_browser_login(args: argparse.Namespace) -> bool:
113
+ return any(
114
+ bool(getattr(args, name, None))
115
+ for name in (
116
+ "browser",
117
+ "device",
118
+ "client_id",
119
+ "authorization_endpoint",
120
+ "token_endpoint",
121
+ "device_authorization_endpoint",
122
+ "scope",
123
+ "credential_field",
124
+ )
125
+ )
126
+
127
+
128
+ def _has_account_login_inputs(args: argparse.Namespace) -> bool:
129
+ return bool(args.email or args.password or bool(args.password_stdin))
130
+
131
+
132
+ def _resolve_login_email(args: argparse.Namespace) -> str:
133
+ email = str(args.email or "").strip()
134
+ if email:
135
+ return email
136
+ if sys.stdin.isatty():
137
+ return input("Qingflow email: ").strip()
138
+ raise QingflowApiError.config_error(
139
+ "qingflow auth login needs an interactive terminal, or pass --email with --password-stdin, or use --browser."
140
+ )
141
+
142
+
143
+ def _resolve_login_password(args: argparse.Namespace) -> str:
144
+ if args.password or bool(args.password_stdin):
145
+ return read_secret_arg(
146
+ args.password,
147
+ stdin_enabled=bool(args.password_stdin),
148
+ label="password",
149
+ )
150
+ if sys.stdin.isatty():
151
+ return getpass.getpass("Qingflow password: ")
152
+ raise QingflowApiError.config_error(
153
+ "qingflow auth login needs an interactive terminal, --password-stdin, or --browser."
154
+ )
155
+
156
+
29
157
  def _handle_use_credential(args: argparse.Namespace, context: CliContext) -> dict:
30
158
  credential = (
31
159
  read_secret_arg(args.credential, stdin_enabled=bool(args.credential_stdin), label="credential")
@@ -32,7 +32,7 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
32
32
 
33
33
  schema_update = schema_subparsers.add_parser("update", help="读取更新记录表结构")
34
34
  schema_update.add_argument("--app-key", required=True)
35
- schema_update.add_argument("--record-id", required=True, type=int)
35
+ schema_update.add_argument("--record-id", required=True)
36
36
  schema_update.set_defaults(handler=_handle_schema_update, format_hint="")
37
37
 
38
38
  schema_import = schema_subparsers.add_parser("import", help="读取导入表结构")
@@ -59,7 +59,7 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
59
59
 
60
60
  get = record_subparsers.add_parser("get", help="读取单条记录")
61
61
  get.add_argument("--app-key", required=True)
62
- get.add_argument("--record-id", required=True, type=int)
62
+ get.add_argument("--record-id", required=True)
63
63
  get.add_argument("--column", dest="columns", action="append", type=int, default=[])
64
64
  get.add_argument("--columns-file")
65
65
  get.add_argument("--view-id")
@@ -73,7 +73,7 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
73
73
 
74
74
  update = record_subparsers.add_parser("update", help="更新记录")
75
75
  update.add_argument("--app-key", required=True)
76
- update.add_argument("--record-id", type=int)
76
+ update.add_argument("--record-id")
77
77
  update.add_argument("--fields-file")
78
78
  update.add_argument("--items-file")
79
79
  update.add_argument("--dry-run", action=argparse.BooleanOptionalAction, default=False)
@@ -82,7 +82,7 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
82
82
 
83
83
  delete = record_subparsers.add_parser("delete", help="删除记录")
84
84
  delete.add_argument("--app-key", required=True)
85
- delete.add_argument("--record-id", type=int)
85
+ delete.add_argument("--record-id")
86
86
  delete.add_argument("--record-ids-file")
87
87
  delete.set_defaults(handler=_handle_delete, format_hint="")
88
88
 
@@ -102,7 +102,7 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
102
102
 
103
103
  code_block = record_subparsers.add_parser("code-block-run", help="执行代码块字段")
104
104
  code_block.add_argument("--app-key", required=True)
105
- code_block.add_argument("--record-id", required=True, type=int)
105
+ code_block.add_argument("--record-id", required=True)
106
106
  code_block.add_argument("--code-block-field", required=True)
107
107
  code_block.add_argument("--role", type=int, default=1)
108
108
  code_block.add_argument("--workflow-node-id", type=int)
@@ -25,7 +25,7 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
25
25
 
26
26
  get = task_subparsers.add_parser("get", help="读取待办详情")
27
27
  get.add_argument("--app-key", required=True)
28
- get.add_argument("--record-id", required=True, type=int)
28
+ get.add_argument("--record-id", required=True)
29
29
  get.add_argument("--workflow-node-id", required=True, type=int)
30
30
  get.add_argument("--include-candidates", action=argparse.BooleanOptionalAction, default=True)
31
31
  get.add_argument("--include-associated-reports", action=argparse.BooleanOptionalAction, default=True)
@@ -33,7 +33,7 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
33
33
 
34
34
  action = task_subparsers.add_parser("action", help="执行待办动作")
35
35
  action.add_argument("--app-key", required=True)
36
- action.add_argument("--record-id", required=True, type=int)
36
+ action.add_argument("--record-id", required=True)
37
37
  action.add_argument("--workflow-node-id", required=True, type=int)
38
38
  action.add_argument("--action", required=True)
39
39
  action.add_argument("--payload-file")
@@ -42,7 +42,7 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
42
42
 
43
43
  log = task_subparsers.add_parser("log", help="读取流程日志")
44
44
  log.add_argument("--app-key", required=True)
45
- log.add_argument("--record-id", required=True, type=int)
45
+ log.add_argument("--record-id", required=True)
46
46
  log.add_argument("--workflow-node-id", required=True, type=int)
47
47
  log.set_defaults(handler=_handle_log, format_hint="")
48
48
 
@@ -40,6 +40,13 @@ def _format_whoami(result: dict[str, Any]) -> str:
40
40
  f"Workspace: {result.get('selected_ws_name') or '-'} ({result.get('selected_ws_id') or '-'})",
41
41
  f"Workspace QF Version: {result.get('qf_version') or '-'}",
42
42
  ]
43
+ cli_auth = result.get("cli_auth") if isinstance(result.get("cli_auth"), dict) else {}
44
+ if cli_auth:
45
+ lines.append(f"Login Flow: {cli_auth.get('flow') or '-'}")
46
+ if cli_auth.get("verification_uri"):
47
+ lines.append(f"Verification URL: {cli_auth.get('verification_uri')}")
48
+ if cli_auth.get("user_code"):
49
+ lines.append(f"User Code: {cli_auth.get('user_code')}")
43
50
  request_route = result.get("request_route") if isinstance(result.get("request_route"), dict) else {}
44
51
  route_qf_version = request_route.get("qf_version")
45
52
  if route_qf_version and route_qf_version != result.get("qf_version"):