@qingflow-tech/qingflow-app-builder-mcp 1.0.2 → 1.0.3

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 +9 -3
  3. package/npm/lib/runtime.mjs +10 -3
  4. package/package.json +1 -1
  5. package/pyproject.toml +1 -1
  6. package/skills/qingflow-app-builder/SKILL.md +88 -184
  7. package/skills/qingflow-app-builder/references/create-app.md +15 -34
  8. package/skills/qingflow-app-builder/references/gotchas.md +3 -3
  9. package/skills/qingflow-app-builder/references/solution-playbooks.md +1 -2
  10. package/skills/qingflow-app-builder/references/tool-selection.md +9 -10
  11. package/src/qingflow_mcp/__init__.py +33 -1
  12. package/src/qingflow_mcp/builder_facade/models.py +14 -4
  13. package/src/qingflow_mcp/builder_facade/service.py +1582 -124
  14. package/src/qingflow_mcp/cli/commands/auth.py +63 -0
  15. package/src/qingflow_mcp/cli/commands/builder.py +4 -3
  16. package/src/qingflow_mcp/cli/commands/record.py +5 -5
  17. package/src/qingflow_mcp/cli/commands/task.py +74 -22
  18. package/src/qingflow_mcp/cli/commands/workspace.py +22 -0
  19. package/src/qingflow_mcp/cli/formatters.py +287 -48
  20. package/src/qingflow_mcp/cli/main.py +6 -1
  21. package/src/qingflow_mcp/cli/qingflow_login.py +116 -0
  22. package/src/qingflow_mcp/config.py +1 -1
  23. package/src/qingflow_mcp/errors.py +2 -2
  24. package/src/qingflow_mcp/id_utils.py +49 -0
  25. package/src/qingflow_mcp/public_surface.py +11 -1
  26. package/src/qingflow_mcp/response_trim.py +380 -9
  27. package/src/qingflow_mcp/server.py +4 -0
  28. package/src/qingflow_mcp/server_app_builder.py +11 -1
  29. package/src/qingflow_mcp/server_app_user.py +24 -0
  30. package/src/qingflow_mcp/session_store.py +69 -15
  31. package/src/qingflow_mcp/solution/compiler/form_compiler.py +2 -2
  32. package/src/qingflow_mcp/solution/executor.py +2 -2
  33. package/src/qingflow_mcp/tools/ai_builder_tools.py +48 -18
  34. package/src/qingflow_mcp/tools/app_tools.py +1 -0
  35. package/src/qingflow_mcp/tools/auth_tools.py +217 -9
  36. package/src/qingflow_mcp/tools/base.py +6 -2
  37. package/src/qingflow_mcp/tools/code_block_tools.py +2 -2
  38. package/src/qingflow_mcp/tools/import_tools.py +36 -2
  39. package/src/qingflow_mcp/tools/record_tools.py +410 -156
  40. package/src/qingflow_mcp/tools/resource_read_tools.py +114 -32
  41. package/src/qingflow_mcp/tools/task_context_tools.py +899 -141
  42. package/src/qingflow_mcp/tools/workspace_tools.py +141 -0
@@ -1,8 +1,12 @@
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 ..qingflow_login import login_with_qingflow_password
6
10
  from .common import read_secret_arg
7
11
 
8
12
 
@@ -10,6 +14,15 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
10
14
  parser = subparsers.add_parser("auth", help="认证与会话")
11
15
  auth_subparsers = parser.add_subparsers(dest="auth_command", required=True)
12
16
 
17
+ login = auth_subparsers.add_parser("login", help="登录 CLI:轻流账号密码交互")
18
+ login.add_argument("--base-url")
19
+ login.add_argument("--qf-version")
20
+ login.add_argument("--email", help="轻流账号邮箱;不传时在交互终端提示输入")
21
+ login.add_argument("--password", help="轻流账号密码;建议仅用于本地调试,脚本请优先使用 --password-stdin")
22
+ login.add_argument("--password-stdin", action="store_true", help="从标准输入读取轻流账号密码")
23
+ login.add_argument("--persist", action=argparse.BooleanOptionalAction, default=True)
24
+ login.set_defaults(handler=_handle_login, format_hint="auth_whoami")
25
+
13
26
  use_credential = auth_subparsers.add_parser("use-credential", help="直接注入 credential")
14
27
  use_credential.add_argument("--base-url")
15
28
  use_credential.add_argument("--qf-version")
@@ -26,6 +39,56 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
26
39
  logout.set_defaults(handler=_handle_logout, format_hint="")
27
40
 
28
41
 
42
+ def _handle_login(args: argparse.Namespace, context: CliContext) -> dict:
43
+ email = _resolve_login_email(args)
44
+ password = _resolve_login_password(args)
45
+ login_result = login_with_qingflow_password(
46
+ base_url=args.base_url,
47
+ email=email,
48
+ password=password,
49
+ )
50
+ result = context.auth.auth_use_token(
51
+ profile=args.profile,
52
+ base_url=args.base_url,
53
+ qf_version=args.qf_version,
54
+ token=login_result.token,
55
+ login_token=login_result.login_token,
56
+ user_info=login_result.user_info,
57
+ persist=bool(args.persist),
58
+ )
59
+ warnings = list(result.get("warnings") or []) if isinstance(result, dict) else []
60
+ if isinstance(result, dict):
61
+ result["cli_auth"] = {"flow": login_result.flow}
62
+ if warnings:
63
+ result["warnings"] = warnings
64
+ return result
65
+
66
+
67
+ def _resolve_login_email(args: argparse.Namespace) -> str:
68
+ email = str(args.email or "").strip()
69
+ if email:
70
+ return email
71
+ if sys.stdin.isatty():
72
+ return input("Qingflow email: ").strip()
73
+ raise QingflowApiError.config_error(
74
+ "qingflow auth login needs an interactive terminal, or pass --email with --password-stdin."
75
+ )
76
+
77
+
78
+ def _resolve_login_password(args: argparse.Namespace) -> str:
79
+ if args.password or bool(args.password_stdin):
80
+ return read_secret_arg(
81
+ args.password,
82
+ stdin_enabled=bool(args.password_stdin),
83
+ label="password",
84
+ )
85
+ if sys.stdin.isatty():
86
+ return getpass.getpass("Qingflow password: ")
87
+ raise QingflowApiError.config_error(
88
+ "qingflow auth login needs an interactive terminal or --password-stdin."
89
+ )
90
+
91
+
29
92
  def _handle_use_credential(args: argparse.Namespace, context: CliContext) -> dict:
30
93
  credential = (
31
94
  read_secret_arg(args.credential, stdin_enabled=bool(args.credential_stdin), label="credential")
@@ -101,7 +101,7 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
101
101
  app_release_lock.add_argument("--lock-owner-name", required=True)
102
102
  app_release_lock.set_defaults(handler=_handle_app_release_edit_lock_if_mine, format_hint="builder_summary")
103
103
 
104
- app_get = app_subparsers.add_parser("get", help="读取应用配置")
104
+ app_get = app_subparsers.add_parser("get", help="读取应用配置(字段请使用: builder app get --app-key APP fields)")
105
105
  app_get.add_argument(
106
106
  "builder_app_get_section",
107
107
  nargs="?",
@@ -160,7 +160,7 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
160
160
  portal_apply.add_argument("--dash-name", default="")
161
161
  portal_apply.add_argument("--package-id", type=int)
162
162
  portal_apply.add_argument("--publish", action=argparse.BooleanOptionalAction, default=True)
163
- portal_apply.add_argument("--sections-file", required=True)
163
+ portal_apply.add_argument("--sections-file")
164
164
  portal_apply.add_argument("--visibility-file")
165
165
  portal_apply.add_argument("--auth-file")
166
166
  portal_apply.add_argument("--icon")
@@ -513,13 +513,14 @@ def _handle_portal_apply(args: argparse.Namespace, context: CliContext) -> dict:
513
513
  "portal apply requires either --dash-key, or --package-id together with --dash-name.",
514
514
  fix_hint="Use `--dash-key` for an existing portal. For create mode, pass `--package-id --dash-name`.",
515
515
  )
516
+ sections = [] if not args.sections_file else require_list_arg(args.sections_file, option_name="--sections-file")
516
517
  return context.builder.portal_apply(
517
518
  profile=args.profile,
518
519
  dash_key=args.dash_key,
519
520
  dash_name=args.dash_name,
520
521
  package_id=args.package_id,
521
522
  publish=bool(args.publish),
522
- sections=require_list_arg(args.sections_file, option_name="--sections-file"),
523
+ sections=sections,
523
524
  visibility=load_object_arg(args.visibility_file, option_name="--visibility-file"),
524
525
  auth=load_object_arg(args.auth_file, option_name="--auth-file"),
525
526
  icon=args.icon,
@@ -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)
@@ -15,32 +15,52 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
15
15
  list_parser.add_argument("--flow-status", default="all")
16
16
  list_parser.add_argument("--app-key")
17
17
  list_parser.add_argument("--workflow-node-id", type=int)
18
- list_parser.add_argument("--query")
18
+ list_parser.add_argument(
19
+ "--query",
20
+ help="先走后端待办检索;当后端返回零结果时,公开 task_list 会回退到本地匹配 app_name / workflow_node_name / app_key / record_id。",
21
+ )
19
22
  list_parser.add_argument("--page", type=int, default=1)
20
23
  list_parser.add_argument("--page-size", type=int, default=20)
21
24
  list_parser.set_defaults(handler=_handle_list, format_hint="task_list")
22
25
 
23
- get = task_subparsers.add_parser("get", help="读取待办详情")
24
- get.add_argument("--app-key", required=True)
25
- get.add_argument("--record-id", required=True, type=int)
26
- get.add_argument("--workflow-node-id", required=True, type=int)
26
+ get = task_subparsers.add_parser("get", help="读取待办详情;推荐直接传 --task-id")
27
+ get.add_argument("--task-id")
28
+ get.add_argument("--app-key")
29
+ get.add_argument("--record-id")
30
+ get.add_argument("--workflow-node-id", type=int)
27
31
  get.add_argument("--include-candidates", action=argparse.BooleanOptionalAction, default=True)
28
32
  get.add_argument("--include-associated-reports", action=argparse.BooleanOptionalAction, default=True)
29
33
  get.set_defaults(handler=_handle_get, format_hint="task_get")
30
34
 
31
35
  action = task_subparsers.add_parser("action", help="执行待办动作")
32
- action.add_argument("--app-key", required=True)
33
- action.add_argument("--record-id", required=True, type=int)
34
- action.add_argument("--workflow-node-id", required=True, type=int)
36
+ action.add_argument("--task-id")
37
+ action.add_argument("--app-key")
38
+ action.add_argument("--record-id")
39
+ action.add_argument("--workflow-node-id", type=int)
35
40
  action.add_argument("--action", required=True)
36
41
  action.add_argument("--payload-file")
37
42
  action.add_argument("--fields-file")
38
- action.set_defaults(handler=_handle_action, format_hint="")
43
+ action.set_defaults(
44
+ handler=_handle_action,
45
+ format_hint="task_action_execute",
46
+ hide_effective_context_line=True,
47
+ )
39
48
 
40
- log = task_subparsers.add_parser("log", help="读取流程日志")
41
- log.add_argument("--app-key", required=True)
42
- log.add_argument("--record-id", required=True, type=int)
43
- log.add_argument("--workflow-node-id", required=True, type=int)
49
+ report = task_subparsers.add_parser("report", help="读取待办关联报表详情;推荐直接传 --task-id")
50
+ report.add_argument("--task-id")
51
+ report.add_argument("--app-key")
52
+ report.add_argument("--record-id")
53
+ report.add_argument("--workflow-node-id", type=int)
54
+ report.add_argument("--report-id", required=True, type=int)
55
+ report.add_argument("--page", type=int, default=1)
56
+ report.add_argument("--page-size", type=int, default=20)
57
+ report.set_defaults(handler=_handle_report, format_hint="task_associated_report_detail_get")
58
+
59
+ log = task_subparsers.add_parser("log", help="读取流程日志;推荐直接传 --task-id")
60
+ log.add_argument("--task-id")
61
+ log.add_argument("--app-key")
62
+ log.add_argument("--record-id")
63
+ log.add_argument("--workflow-node-id", type=int)
44
64
  log.set_defaults(handler=_handle_log, format_hint="")
45
65
 
46
66
 
@@ -58,22 +78,32 @@ def _handle_list(args: argparse.Namespace, context: CliContext) -> dict:
58
78
 
59
79
 
60
80
  def _handle_get(args: argparse.Namespace, context: CliContext) -> dict:
81
+ if not args.task_id and not (args.app_key and args.record_id and args.workflow_node_id):
82
+ raise RuntimeError(
83
+ '{"category":"config","message":"task get requires --task-id, or --app-key together with --record-id and --workflow-node-id"}'
84
+ )
61
85
  return context.task.task_get(
62
86
  profile=args.profile,
63
- app_key=args.app_key,
64
- record_id=args.record_id,
65
- workflow_node_id=args.workflow_node_id,
87
+ task_id=args.task_id,
88
+ app_key=args.app_key or "",
89
+ record_id=args.record_id or "",
90
+ workflow_node_id=int(args.workflow_node_id or 0),
66
91
  include_candidates=bool(args.include_candidates),
67
92
  include_associated_reports=bool(args.include_associated_reports),
68
93
  )
69
94
 
70
95
 
71
96
  def _handle_action(args: argparse.Namespace, context: CliContext) -> dict:
97
+ if not args.task_id and not (args.app_key and args.record_id and args.workflow_node_id):
98
+ raise RuntimeError(
99
+ '{"category":"config","message":"task action requires --task-id, or --app-key together with --record-id and --workflow-node-id"}'
100
+ )
72
101
  return context.task.task_action_execute(
73
102
  profile=args.profile,
74
- app_key=args.app_key,
75
- record_id=args.record_id,
76
- workflow_node_id=args.workflow_node_id,
103
+ task_id=args.task_id,
104
+ app_key=args.app_key or "",
105
+ record_id=args.record_id or "",
106
+ workflow_node_id=int(args.workflow_node_id or 0),
77
107
  action=args.action,
78
108
  payload=load_object_arg(args.payload_file, option_name="--payload-file") or {},
79
109
  fields=load_object_arg(args.fields_file, option_name="--fields-file") or {},
@@ -81,9 +111,31 @@ def _handle_action(args: argparse.Namespace, context: CliContext) -> dict:
81
111
 
82
112
 
83
113
  def _handle_log(args: argparse.Namespace, context: CliContext) -> dict:
114
+ if not args.task_id and not (args.app_key and args.record_id and args.workflow_node_id):
115
+ raise RuntimeError(
116
+ '{"category":"config","message":"task log requires --task-id, or --app-key together with --record-id and --workflow-node-id"}'
117
+ )
84
118
  return context.task.task_workflow_log_get(
85
119
  profile=args.profile,
86
- app_key=args.app_key,
87
- record_id=args.record_id,
88
- workflow_node_id=args.workflow_node_id,
120
+ task_id=args.task_id,
121
+ app_key=args.app_key or "",
122
+ record_id=args.record_id or "",
123
+ workflow_node_id=int(args.workflow_node_id or 0),
124
+ )
125
+
126
+
127
+ def _handle_report(args: argparse.Namespace, context: CliContext) -> dict:
128
+ if not args.task_id and not (args.app_key and args.record_id and args.workflow_node_id):
129
+ raise RuntimeError(
130
+ '{"category":"config","message":"task report requires --task-id, or --app-key together with --record-id and --workflow-node-id"}'
131
+ )
132
+ return context.task.task_associated_report_detail_get(
133
+ profile=args.profile,
134
+ task_id=args.task_id,
135
+ app_key=args.app_key or "",
136
+ record_id=args.record_id or "",
137
+ workflow_node_id=int(args.workflow_node_id or 0),
138
+ report_id=int(args.report_id),
139
+ page=int(args.page),
140
+ page_size=int(args.page_size),
89
141
  )
@@ -15,6 +15,14 @@ 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
+ 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
+
22
+ select_parser = workspace_subparsers.add_parser("select", help="切换当前工作区")
23
+ select_parser.add_argument("--ws-id", type=int, required=True)
24
+ select_parser.set_defaults(handler=_handle_select, format_hint="workspace_get")
25
+
18
26
 
19
27
  def _handle_list(args: argparse.Namespace, context: CliContext) -> dict:
20
28
  return context.workspace.workspace_list(
@@ -23,3 +31,17 @@ def _handle_list(args: argparse.Namespace, context: CliContext) -> dict:
23
31
  page_size=args.page_size,
24
32
  include_external=bool(args.include_external),
25
33
  )
34
+
35
+
36
+ def _handle_get(args: argparse.Namespace, context: CliContext) -> dict:
37
+ return context.workspace.workspace_get(
38
+ profile=args.profile,
39
+ ws_id=args.ws_id if int(args.ws_id or 0) > 0 else None,
40
+ )
41
+
42
+
43
+ def _handle_select(args: argparse.Namespace, context: CliContext) -> dict:
44
+ return context.workspace.workspace_select(
45
+ profile=args.profile,
46
+ ws_id=int(args.ws_id),
47
+ )