@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 +2 -2
- package/docs/local-agent-install.md +7 -0
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/src/qingflow_mcp/__init__.py +1 -1
- package/src/qingflow_mcp/cli/commands/auth.py +128 -0
- package/src/qingflow_mcp/cli/commands/record.py +5 -5
- package/src/qingflow_mcp/cli/commands/task.py +3 -3
- package/src/qingflow_mcp/cli/formatters.py +7 -0
- package/src/qingflow_mcp/cli/oauth_login.py +626 -0
- package/src/qingflow_mcp/cli/qingflow_login.py +116 -0
- package/src/qingflow_mcp/errors.py +2 -2
- package/src/qingflow_mcp/id_utils.py +49 -0
- package/src/qingflow_mcp/public_surface.py +1 -0
- package/src/qingflow_mcp/response_trim.py +10 -0
- package/src/qingflow_mcp/tools/auth_tools.py +130 -0
- package/src/qingflow_mcp/tools/base.py +1 -1
- package/src/qingflow_mcp/tools/code_block_tools.py +2 -2
- package/src/qingflow_mcp/tools/record_tools.py +96 -55
- package/src/qingflow_mcp/tools/task_context_tools.py +55 -44
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.
|
|
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.
|
|
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
package/pyproject.toml
CHANGED
|
@@ -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
|
|
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
|
|
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"
|
|
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"
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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"):
|