@qingflow-tech/qingflow-cli 1.1.61-beta.1 → 1.1.61-beta.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.
- package/README.md +3 -3
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/skills/qingflow-cli/SKILL.md +11 -18
- package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_DATA_RETRIEVAL_WORKFLOW.md +3 -3
- package/src/qingflow_mcp/cli/main.py +34 -1
- package/src/qingflow_mcp/cloud_context.py +16 -1
- package/src/qingflow_mcp/public_surface.py +67 -8
- package/src/qingflow_mcp/server.py +12 -6
- package/src/qingflow_mcp/server_app_builder.py +9 -5
- package/src/qingflow_mcp/server_app_user.py +9 -5
- package/src/qingflow_mcp/server_cloud.py +43 -7
- package/src/qingflow_mcp/tool_catalog.py +387 -0
- package/src/qingflow_mcp/tool_catalog_i18n.py +185 -0
- package/src/qingflow_mcp/tool_policy.py +197 -0
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@ Qingflow CLI 是轻流面向开发者和 AI Agent 的命令行工具。它把轻
|
|
|
7
7
|
## 安装
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
|
-
npm install @qingflow-tech/qingflow-cli@1.1.61-beta.
|
|
10
|
+
npm install @qingflow-tech/qingflow-cli@1.1.61-beta.3
|
|
11
11
|
```
|
|
12
12
|
|
|
13
13
|
推荐从官网复制这一条一键安装 Qingflow CLI:
|
|
@@ -21,7 +21,7 @@ npx qingflow-cli@latest install
|
|
|
21
21
|
## 运行
|
|
22
22
|
|
|
23
23
|
```bash
|
|
24
|
-
npx -y -p @qingflow-tech/qingflow-cli@1.1.61-beta.
|
|
24
|
+
npx -y -p @qingflow-tech/qingflow-cli@1.1.61-beta.3 qingflow
|
|
25
25
|
```
|
|
26
26
|
|
|
27
27
|
全局安装后可以直接运行:
|
|
@@ -62,7 +62,7 @@ qingflow-skills install --agent codex --scope user
|
|
|
62
62
|
如果你通过一次性 `npx -p` 执行 skills 安装,建议加 `--copy`,避免 symlink 指向 npm 临时执行缓存:
|
|
63
63
|
|
|
64
64
|
```bash
|
|
65
|
-
npx -y -p @qingflow-tech/qingflow-cli@1.1.61-beta.
|
|
65
|
+
npx -y -p @qingflow-tech/qingflow-cli@1.1.61-beta.3 qingflow-skills install --agent codex --scope user --copy
|
|
66
66
|
```
|
|
67
67
|
|
|
68
68
|
`qingflow-skills install` 默认使用 symlink,且遇到同名 skill 不会覆盖;只有显式传入 `--force` 才会替换。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@qingflow-tech/qingflow-cli",
|
|
3
|
-
"version": "1.1.61-beta.
|
|
3
|
+
"version": "1.1.61-beta.3",
|
|
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
|
@@ -23,7 +23,7 @@ description: |
|
|
|
23
23
|
| [reference/00-INDEX.md](./reference/00-INDEX.md) | **总导航**:按用户目标选择 record / task / builder 文档;复杂任务先读 |
|
|
24
24
|
| [reference/QINGFLOW_CLI_MEMBER_CHEATSHEET.md](./reference/core/QINGFLOW_CLI_MEMBER_CHEATSHEET.md) | **普通成员**主线:最短路径、`view_id`、与管理命令的差异预期 |
|
|
25
25
|
| [reference/QINGFLOW_CLI_FIELD_DATA_TYPES.md](./reference/core/QINGFLOW_CLI_FIELD_DATA_TYPES.md) | **字段数据类型**:`record schema …` 的 `fields[]`(`field_id`、`title`、`kind`、`options`…);`record list/get/access` 与写入 payload 的形态对照 |
|
|
26
|
-
| [reference/QINGFLOW_CLI_DATA_RETRIEVAL_WORKFLOW.md](./reference/core/QINGFLOW_CLI_DATA_RETRIEVAL_WORKFLOW.md) |
|
|
26
|
+
| [reference/QINGFLOW_CLI_DATA_RETRIEVAL_WORKFLOW.md](./reference/core/QINGFLOW_CLI_DATA_RETRIEVAL_WORKFLOW.md) | **普通应用数据读取**:`data query` 获取记录和 schema → 按需 `data get` 读取单条;附件下载见 [data 操作说明](./reference/data/README.md) |
|
|
27
27
|
| [reference/QINGFLOW_CLI_TASK_CONTEXT_WORKFLOW.md](./reference/task/QINGFLOW_CLI_TASK_CONTEXT_WORKFLOW.md) | **任务(待办已办)上下文 SOP**:`task list` → `task get` → `task log` → `task report` → `task action`;只把 `task_id` 作为主定位参数 |
|
|
28
28
|
| [reference/QINGFLOW_CLI_RECORD_IMPORT_WORKFLOW.md](./reference/record/QINGFLOW_CLI_RECORD_IMPORT_WORKFLOW.md) | **批量导入 SOP**:`app_get`(**`import_capability`**)→ `record_import_template_get` → `record_import_verify` →(可选)`record_import_repair_local` → `record_import_start` → `record_import_status_get`;CLI `import` / `record schema import` |
|
|
29
29
|
| [reference/builder/README.md](./reference/builder/README.md) | **Builder 总导航**:完整系统、单应用、字段、布局、视图、报表、门户、按钮/关联资源、流程、发布校验 |
|
|
@@ -40,7 +40,7 @@ description: |
|
|
|
40
40
|
| [reference/QINGFLOW_CLI_RECORD_CREATE_WORKFLOW.md](./reference/record/QINGFLOW_CLI_RECORD_CREATE_WORKFLOW.md) | **新建记录**:`record schema insert -> record insert --items-file`;成员/部门/关联优先自然语言输入;细节见 [record/insert](./reference/record/insert/README.md) |
|
|
41
41
|
| [reference/QINGFLOW_CLI_RECORD_UPDATE_WORKFLOW.md](./reference/record/QINGFLOW_CLI_RECORD_UPDATE_WORKFLOW.md) | **更新记录**:主链路是 `record get` → 智能体按 `fields[]` 组 key/value → **`record update`**;`record schema update` 仅用于失败后诊断可写字段/路径 |
|
|
42
42
|
| [reference/QINGFLOW_CLI_RECORD_DELETE_WORKFLOW.md](./reference/record/QINGFLOW_CLI_RECORD_DELETE_WORKFLOW.md) | **删除记录**:`record list/get` 精确定位 → `record delete --app-key --record-id --view-id system:*`;批量用 `--record-ids-file` |
|
|
43
|
-
| [reference/record/analysis/README.md](./reference/record/analysis/README.md) | **数据分析**:最终统计结论、分析报告、趋势/排名/比例/分布统一走 `
|
|
43
|
+
| [reference/record/analysis/README.md](./reference/record/analysis/README.md) | **数据分析**:最终统计结论、分析报告、趋势/排名/比例/分布统一走 `data query --all --output csv -> Python/pandas` |
|
|
44
44
|
| [reference/builder/reference/app-delivery-sop.md](./reference/builder/reference/app-delivery-sop.md) | **底层交付参考**:历史完整 SOP、权限、系统列和工具细节;不是主入口 |
|
|
45
45
|
| [reference/builder/reference/workspace-icons.md](./reference/builder/reference/workspace-icons.md) | **图标参考**:`builder icon catalog`;新建应用包、应用、门户必须显式选择非 `template` 的 `icon + color` |
|
|
46
46
|
| [reference/builder/reference/match-rules.md](./reference/builder/reference/match-rules.md) | **统一筛选 / 条件写法**:`filters`、`visible_when`、QingBI 图表筛选、工作流条件、`match_mappings`、`field_mappings` 的 `operator/value` 规则 |
|
|
@@ -129,18 +129,11 @@ qingflow --json data query --app-key APP_KEY > tmp/qingflow_data_query.json
|
|
|
129
129
|
|
|
130
130
|
---
|
|
131
131
|
|
|
132
|
-
##
|
|
132
|
+
## 应用数据:查询与读取(默认路径)
|
|
133
133
|
|
|
134
|
-
|
|
134
|
+
已知 `app_key` 时直接执行 `qingflow --json data query --app-key <APP_KEY>`;首次查询保留默认 schema,根据响应中的 `field_capabilities` 和 `hints` 决定筛选、分页或停止。定位到 `record_id` 后,按需执行 `qingflow --json data get --app-key <APP_KEY> --record-id <RECORD_ID>`;下载附件时加 `--download-attachments`。
|
|
135
135
|
|
|
136
|
-
|
|
137
|
-
2. **`record_schema_get`(`schema_mode=browse`)**:`qingflow --json record schema browse --app-key <APP_KEY> --view-id <VIEW_ID>`(CLI 子命令 **`browse`** 即浏览视图表结构)。
|
|
138
|
-
3. **`record_list` / `record_get` / `record_logs_get`**:`record list …` 用于样本浏览或模糊定位候选;`record get --record-id …` 下钻单条前端详情页首屏上下文(字段、首屏日志、引用、图片与文件资产);只有需要完整审计历史时,再用 `record logs --record-id …` 读取全量数据日志和流程日志 JSONL。
|
|
139
|
-
|
|
140
|
-
`app get` 里的 `CUSTOM_VIEW_LIST_UNAVAILABLE` 表示自定义视图列表读取被权限或后端限制降级;若 `accessible_views` 中仍有 `system:*`,可继续用该视图读数。若没有任何视图,再回到前端 URL、`app list` 可见应用、或用户提供的 `viewgraphKey` 补充上下文,不要直接断言应用不可读。
|
|
141
|
-
`app get.data.can_create=false` 才表示 applicant/create 路径不可用;创建权限探测遇到 auth / server 等非权限类错误时应直接按失败处理,不要包装成“未知创建权限”或“用户无新建权限”。
|
|
142
|
-
|
|
143
|
-
**细则、响应解包、`view_id` 雷区与命令模板**见 **[reference/QINGFLOW_CLI_DATA_RETRIEVAL_WORKFLOW.md](./reference/core/QINGFLOW_CLI_DATA_RETRIEVAL_WORKFLOW.md)**(编写前已对当前 CLI **实跑**校验)。若仅需「最快看一眼有没有行」,可省略 **`schema browse`**,直接走下文「最短路径」。如果用户要任何最终统计结论、分析报告、趋势/排名/比例/分布,按 **[record/analysis](./reference/record/analysis/README.md)** 执行 `record access -> Python/pandas`。
|
|
136
|
+
参数与附件处理见 [data 操作说明](./reference/data/README.md)。详情页引用、关联资源和完整日志见 [record 专项参考](./reference/record/QINGFLOW_CLI_RECORD_SPECIALIZED_WORKFLOW.md);最终统计结论按 [数据分析说明](./reference/record/analysis/README.md) 使用 `data query --all --output csv`。
|
|
144
137
|
|
|
145
138
|
---
|
|
146
139
|
|
|
@@ -167,7 +160,7 @@ qingflow --json data query --app-key APP_KEY > tmp/qingflow_data_query.json
|
|
|
167
160
|
|
|
168
161
|
**更新路径**:`record update` 会自动先尝试数据管理员直改,再 fallback 到前端同源的 custom view 详情编辑路径;如果当前用户存在这条记录的唯一待办且目标字段在当前节点可编辑,最后会尝试 workflow save-only。成功时只需要读 **`status / update_route / verification_status`**;失败时再读失败原因与路线诊断。不要把中间 route 或辅助 schema/readback 探测的 40002 / 40027 / 404 当作最终失败。
|
|
169
162
|
|
|
170
|
-
**大号 `record_id`(常见于 Snowflake 类 id)**:若干 CLI 版本在 **`record update` 写入路径**上对 `apply_id` 会做 **JavaScript 安全整数**相关校验,`list`/`get`/`schema update` 仍可能正常而 **更新落库报错**(常为 `apply_id must be positive`)。排障见 **[reference/QINGFLOW_CLI_RECORD_UPDATE_WORKFLOW.md](./reference/record/QINGFLOW_CLI_RECORD_UPDATE_WORKFLOW.md)**。业务统计与最终结论仍必须按 **[record/analysis](./reference/record/analysis/README.md)** 使用 `
|
|
163
|
+
**大号 `record_id`(常见于 Snowflake 类 id)**:若干 CLI 版本在 **`record update` 写入路径**上对 `apply_id` 会做 **JavaScript 安全整数**相关校验,`list`/`get`/`schema update` 仍可能正常而 **更新落库报错**(常为 `apply_id must be positive`)。排障见 **[reference/QINGFLOW_CLI_RECORD_UPDATE_WORKFLOW.md](./reference/record/QINGFLOW_CLI_RECORD_UPDATE_WORKFLOW.md)**。业务统计与最终结论仍必须按 **[record/analysis](./reference/record/analysis/README.md)** 使用 `data query --all --output csv -> Python/pandas`。
|
|
171
164
|
|
|
172
165
|
**命令模板、批量 `items` 形态、与新建相同的写入值约定**(按 **`kind`** 分题型见 **[QINGFLOW_CLI_FIELD_DATA_TYPES.md](./reference/core/QINGFLOW_CLI_FIELD_DATA_TYPES.md)**)见 **[reference/QINGFLOW_CLI_RECORD_UPDATE_WORKFLOW.md](./reference/record/QINGFLOW_CLI_RECORD_UPDATE_WORKFLOW.md)**。
|
|
173
166
|
|
|
@@ -242,16 +235,16 @@ qingflow --json data query \
|
|
|
242
235
|
|
|
243
236
|
- `data insert/update/delete` 是普通应用数据直接写入入口。写入前用只读 `data query/get` 获取 schema、字段能力与访问范围;删除、覆盖已有值、批量写入或其他不可逆操作必须先确认目标和影响范围。
|
|
244
237
|
- `task`、builder、import 均按路由表进入专项文档;主 Skill 不复制其参数、枚举或完整 SOP。
|
|
245
|
-
-
|
|
238
|
+
- 导出/下载只有用户明确需要文件时才使用,并按对应命令的参数执行;不要把导出当作普通查询或统计分析默认入口。
|
|
246
239
|
- 查看视图时,`baseInfo` 或 `viewConfig` 只有 40002 / 40027 / 404 这类权限/缺失边界可用另一侧降级,非权限类后端错误不隐藏;完整规则见 Builder 视图文档。
|
|
247
240
|
|
|
248
|
-
##
|
|
241
|
+
## 专项上下文
|
|
249
242
|
|
|
250
|
-
|
|
243
|
+
详情页引用、关联资源、完整数据/流程日志或代码块字段执行见 [record 专项参考](./reference/record/QINGFLOW_CLI_RECORD_SPECIALIZED_WORKFLOW.md)。用户明确指定 `record` 命令时,阅读对应的 `reference/record/` 文档。
|
|
251
244
|
|
|
252
|
-
`data query/get` 默认只读取附件字段。用户需要下载或读取附件时,保持相同 profile 和工作区,对已定位的同一 `app_key`、`record_id` 执行 `qingflow --json data get --download-attachments`,保留 schema,并使用 `file_assets.items[].local_path`。该入口直接处理存储 Cookie
|
|
245
|
+
`data query/get` 默认只读取附件字段。用户需要下载或读取附件时,保持相同 profile 和工作区,对已定位的同一 `app_key`、`record_id` 执行 `qingflow --json data get --download-attachments`,保留 schema,并使用 `file_assets.items[].local_path`。该入口直接处理存储 Cookie、重定向和数据面链接刷新。不要用 curl、requests 或 urllib 直接请求远端 Qingflow 附件 URL,也不要为下载而读取认证配置或打印 token。
|
|
253
246
|
|
|
254
|
-
|
|
247
|
+
附件下载阅读 [data 附件下载](./reference/data/README.md#附件下载与读取)。
|
|
255
248
|
|
|
256
249
|
## 关联入口
|
|
257
250
|
|
|
@@ -14,7 +14,7 @@ data query(默认带 schema)
|
|
|
14
14
|
→ 已知 record_id 时按需 data get
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
-
已知 `app_key` 时直接 query
|
|
17
|
+
已知 `app_key` 时直接 query;未知时先用 `app list --query` 定位应用。
|
|
18
18
|
|
|
19
19
|
## 1. 首次查询与字段能力
|
|
20
20
|
|
|
@@ -33,7 +33,7 @@ qingflow --json data query --app-key APP_KEY \
|
|
|
33
33
|
- `access`:本次实际访问范围;
|
|
34
34
|
- `hints`:需要收窄、继续、停止或调整参数的动态提示。
|
|
35
35
|
|
|
36
|
-
不要用 insert/update
|
|
36
|
+
不要用 insert/update 探测字段能力。
|
|
37
37
|
|
|
38
38
|
## 2. 字段筛选
|
|
39
39
|
|
|
@@ -96,7 +96,7 @@ qingflow --json data get --app-key APP_KEY --record-id RECORD_ID \
|
|
|
96
96
|
> tmp/qingflow_data_get.json
|
|
97
97
|
```
|
|
98
98
|
|
|
99
|
-
`data get` 默认只读取业务字段。需要下载或读取附件时,对同一 `app_key`、`record_id` 加 `--download-attachments`,保留 schema,并读取 `file_assets.items[].local_path
|
|
99
|
+
`data get` 默认只读取业务字段。需要下载或读取附件时,对同一 `app_key`、`record_id` 加 `--download-attachments`,保留 schema,并读取 `file_assets.items[].local_path`。不要用 Python/curl 直接请求远端附件 URL。状态、预算与完整流程见 [附件下载与读取](../data/README.md#附件下载与读取);完整日志按 record 专项执行。
|
|
100
100
|
|
|
101
101
|
## 6. 统计分析
|
|
102
102
|
|
|
@@ -7,11 +7,22 @@ import sys
|
|
|
7
7
|
from typing import Any, Callable, TextIO
|
|
8
8
|
|
|
9
9
|
from ..errors import QingflowApiError, backend_code_value_int, message_looks_like_invalid_token
|
|
10
|
-
from ..public_surface import
|
|
10
|
+
from ..public_surface import (
|
|
11
|
+
NON_TOOL_CLI_HANDLER_ROUTES,
|
|
12
|
+
cli_public_tool_spec_from_namespace,
|
|
13
|
+
cli_route_from_namespace,
|
|
14
|
+
cli_trim_key_from_namespace,
|
|
15
|
+
)
|
|
11
16
|
from ..resource_uri import cli_resource_base_url
|
|
12
17
|
from ..response_trim import resolve_cli_tool_name, trim_error_response, trim_public_response
|
|
13
18
|
from ..tools.ai_builder_tools import _attach_builder_apply_envelope
|
|
14
19
|
from ..version import get_cli_version, get_cli_version_info
|
|
20
|
+
from ..tool_policy import (
|
|
21
|
+
TOOL_POLICY_ENV,
|
|
22
|
+
TOOL_POLICY_INVALID,
|
|
23
|
+
ToolPolicyViolation,
|
|
24
|
+
parse_tool_policy,
|
|
25
|
+
)
|
|
15
26
|
from .context import CliContext, build_cli_context
|
|
16
27
|
from .formatters import emit_json_result, emit_text_result
|
|
17
28
|
from .commands import register_all_commands
|
|
@@ -90,6 +101,14 @@ def run(
|
|
|
90
101
|
if handler is None:
|
|
91
102
|
parser.print_help(out)
|
|
92
103
|
return 2
|
|
104
|
+
violation = _cli_tool_policy_violation(args)
|
|
105
|
+
if violation is not None:
|
|
106
|
+
return _emit_error(
|
|
107
|
+
violation.payload(),
|
|
108
|
+
json_mode=bool(args.json),
|
|
109
|
+
stdout=out,
|
|
110
|
+
stderr=err,
|
|
111
|
+
)
|
|
93
112
|
try:
|
|
94
113
|
if _should_force_json_output(args):
|
|
95
114
|
setattr(args, "json", True)
|
|
@@ -122,6 +141,20 @@ def run(
|
|
|
122
141
|
return exit_code
|
|
123
142
|
|
|
124
143
|
|
|
144
|
+
def _cli_tool_policy_violation(args: argparse.Namespace) -> ToolPolicyViolation | None:
|
|
145
|
+
stable_id = cli_trim_key_from_namespace(args)
|
|
146
|
+
if stable_id is None:
|
|
147
|
+
# 仅显式登记的非业务分发器可跳过闸门;无子命令的新增 handler 也必须默认拒绝。
|
|
148
|
+
if cli_route_from_namespace(args) in NON_TOOL_CLI_HANDLER_ROUTES:
|
|
149
|
+
return None
|
|
150
|
+
return ToolPolicyViolation(
|
|
151
|
+
TOOL_POLICY_INVALID,
|
|
152
|
+
"user:unregistered",
|
|
153
|
+
"CLI 业务命令未登记稳定工具 ID",
|
|
154
|
+
)
|
|
155
|
+
return parse_tool_policy(os.getenv(TOOL_POLICY_ENV)).violation_for(stable_id)
|
|
156
|
+
|
|
157
|
+
|
|
125
158
|
def _handle_version(_args: argparse.Namespace, _context: CliContext) -> dict[str, Any]:
|
|
126
159
|
info = get_cli_version_info()
|
|
127
160
|
return {
|
|
@@ -2,11 +2,14 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from contextvars import ContextVar
|
|
4
4
|
from dataclasses import dataclass, field
|
|
5
|
-
from typing import Callable
|
|
5
|
+
from typing import TYPE_CHECKING, Callable
|
|
6
6
|
|
|
7
7
|
from .backend_client import BackendRequestContext
|
|
8
8
|
from .session_store import BackendSession, SessionProfile
|
|
9
9
|
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from .tool_policy import ToolPolicySnapshot
|
|
12
|
+
|
|
10
13
|
|
|
11
14
|
CLOUD_PROFILE_NAME = "__cloud_mcp__"
|
|
12
15
|
|
|
@@ -73,3 +76,15 @@ current_call_platform: ContextVar[str] = ContextVar(
|
|
|
73
76
|
"qingflow_mcp_current_call_platform",
|
|
74
77
|
default="",
|
|
75
78
|
)
|
|
79
|
+
|
|
80
|
+
# 远端 MCP 每个 HTTP 请求独立绑定策略原文;缺头用 None 表示,不回退进程环境变量。
|
|
81
|
+
current_tool_policy_header: ContextVar[str | None] = ContextVar(
|
|
82
|
+
"qingflow_mcp_current_tool_policy_header",
|
|
83
|
+
default=None,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
# 在 HTTP 入口合成一次快照,列表与调用共用,避免将 URL 选择保存到会话或共享环境。
|
|
87
|
+
current_http_tool_policy: ContextVar[ToolPolicySnapshot | None] = ContextVar(
|
|
88
|
+
"qingflow_mcp_current_http_tool_policy",
|
|
89
|
+
default=None,
|
|
90
|
+
)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import argparse
|
|
3
4
|
from argparse import Namespace
|
|
4
5
|
from dataclasses import dataclass
|
|
5
6
|
|
|
@@ -109,6 +110,7 @@ USER_PUBLIC_TOOL_SPECS: tuple[PublicToolSpec, ...] = (
|
|
|
109
110
|
PublicToolSpec(USER_DOMAIN, "record_export_direct", ("record_export_direct",), ("export", "direct"), cli_show_effective_context=True),
|
|
110
111
|
PublicToolSpec(USER_DOMAIN, "record_code_block_run", ("record_code_block_run",), ("record", "code-block-run"), cli_show_effective_context=True, cli_context_write=True),
|
|
111
112
|
PublicToolSpec(USER_DOMAIN, "task_list", ("task_list",), ("task", "list"), cli_show_effective_context=True),
|
|
113
|
+
PublicToolSpec(USER_DOMAIN, "task_workbench", cli_route=("task",), mcp_public=False),
|
|
112
114
|
PublicToolSpec(USER_DOMAIN, "task_get", ("task_get",), ("task", "get"), cli_show_effective_context=True),
|
|
113
115
|
PublicToolSpec(USER_DOMAIN, "task_action_execute", ("task_action_execute",), ("task", "action"), cli_show_effective_context=True, cli_context_write=True),
|
|
114
116
|
PublicToolSpec(
|
|
@@ -130,12 +132,12 @@ USER_PUBLIC_TOOL_SPECS: tuple[PublicToolSpec, ...] = (
|
|
|
130
132
|
|
|
131
133
|
|
|
132
134
|
BUILDER_PUBLIC_TOOL_SPECS: tuple[PublicToolSpec, ...] = (
|
|
133
|
-
PublicToolSpec(BUILDER_DOMAIN, "auth_use_credential", ("auth_use_credential",),
|
|
134
|
-
PublicToolSpec(BUILDER_DOMAIN, "auth_whoami", ("auth_whoami",),
|
|
135
|
-
PublicToolSpec(BUILDER_DOMAIN, "auth_logout", ("auth_logout",),
|
|
135
|
+
PublicToolSpec(BUILDER_DOMAIN, "auth_use_credential", ("auth_use_credential",), cli_public=False),
|
|
136
|
+
PublicToolSpec(BUILDER_DOMAIN, "auth_whoami", ("auth_whoami",), cli_public=False),
|
|
137
|
+
PublicToolSpec(BUILDER_DOMAIN, "auth_logout", ("auth_logout",), cli_public=False),
|
|
136
138
|
PublicToolSpec(BUILDER_DOMAIN, "get_login_link", ("get_login_link",), mcp_public=True, cli_public=False),
|
|
137
|
-
PublicToolSpec(BUILDER_DOMAIN, "workspace_list", ("workspace_list",),
|
|
138
|
-
PublicToolSpec(BUILDER_DOMAIN, "workspace_get", ("workspace_get",),
|
|
139
|
+
PublicToolSpec(BUILDER_DOMAIN, "workspace_list", ("workspace_list",), cli_public=False),
|
|
140
|
+
PublicToolSpec(BUILDER_DOMAIN, "workspace_get", ("workspace_get",), cli_public=False),
|
|
139
141
|
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),
|
|
140
142
|
PublicToolSpec(BUILDER_DOMAIN, "feedback_submit", ("feedback_submit",), ("builder", "feedback", "submit"), has_contract=True),
|
|
141
143
|
PublicToolSpec(BUILDER_DOMAIN, "builder_tool_contract", ("builder_tool_contract",), ("builder", "contract"), has_contract=False),
|
|
@@ -194,7 +196,8 @@ PUBLIC_TOOL_BY_KEY: dict[str, PublicToolSpec] = {spec.trim_key: spec for spec in
|
|
|
194
196
|
PUBLIC_TOOL_BY_CLI_ROUTE: dict[tuple[str, ...], PublicToolSpec] = {
|
|
195
197
|
spec.cli_route: spec
|
|
196
198
|
for spec in ALL_PUBLIC_TOOL_SPECS
|
|
197
|
-
|
|
199
|
+
# 隐藏命令仍有可达 handler,闸门绑定不能把“帮助不可见”误当成“不可执行”。
|
|
200
|
+
if spec.cli_route is not None
|
|
198
201
|
}
|
|
199
202
|
PUBLIC_TOOL_BY_CLI_ROUTE.update(
|
|
200
203
|
{
|
|
@@ -205,6 +208,9 @@ PUBLIC_TOOL_BY_CLI_ROUTE.update(
|
|
|
205
208
|
}
|
|
206
209
|
)
|
|
207
210
|
|
|
211
|
+
# 这些 handler 只负责版本输出或参数迁移提示,不会发起轻流业务请求。
|
|
212
|
+
NON_TOOL_CLI_HANDLER_ROUTES = frozenset({("version",), ("record", "schema")})
|
|
213
|
+
|
|
208
214
|
|
|
209
215
|
def public_tool_specs(domain: str | None = None) -> tuple[PublicToolSpec, ...]:
|
|
210
216
|
if domain is None:
|
|
@@ -258,11 +264,13 @@ def cli_route_from_namespace(args: Namespace) -> tuple[str, ...] | None:
|
|
|
258
264
|
record_command = getattr(args, "record_command", None)
|
|
259
265
|
if record_command == "schema":
|
|
260
266
|
schema_command = getattr(args, "record_schema_command", None)
|
|
261
|
-
|
|
267
|
+
if isinstance(schema_command, str) and schema_command:
|
|
268
|
+
return (command, "schema", schema_command)
|
|
269
|
+
return (command, "schema")
|
|
262
270
|
return (command, record_command) if isinstance(record_command, str) and record_command else None
|
|
263
271
|
if command == "task":
|
|
264
272
|
task_command = getattr(args, "task_command", None)
|
|
265
|
-
return (command, task_command) if isinstance(task_command, str) and task_command else
|
|
273
|
+
return (command, task_command) if isinstance(task_command, str) and task_command else (command,)
|
|
266
274
|
if command != "builder":
|
|
267
275
|
child_attr = f"{command}_command"
|
|
268
276
|
child = getattr(args, child_attr, None)
|
|
@@ -283,3 +291,54 @@ def cli_route_from_namespace(args: Namespace) -> tuple[str, ...] | None:
|
|
|
283
291
|
child_attr = f"builder_{builder_command.replace('-', '_')}_command"
|
|
284
292
|
child = getattr(args, child_attr, None)
|
|
285
293
|
return ("builder", builder_command, child) if isinstance(child, str) and child else None
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def discover_cli_handler_routes(
|
|
297
|
+
parser: argparse.ArgumentParser,
|
|
298
|
+
) -> dict[tuple[str, ...], tuple[str, ...]]:
|
|
299
|
+
"""从 argparse 实际 handler 树枚举规范化路由及其实现,包含中间交互入口。"""
|
|
300
|
+
discovered: dict[tuple[str, ...], set[str]] = {}
|
|
301
|
+
|
|
302
|
+
def walk(current: argparse.ArgumentParser, path: tuple[str, ...], inherited: dict[str, object]) -> None:
|
|
303
|
+
attrs = dict(inherited)
|
|
304
|
+
for action in current._actions:
|
|
305
|
+
if isinstance(action, argparse._SubParsersAction):
|
|
306
|
+
continue
|
|
307
|
+
if action.dest != argparse.SUPPRESS and action.default != argparse.SUPPRESS:
|
|
308
|
+
attrs.setdefault(action.dest, action.default)
|
|
309
|
+
attrs.update(current._defaults)
|
|
310
|
+
|
|
311
|
+
handler = current._defaults.get("handler")
|
|
312
|
+
if callable(handler):
|
|
313
|
+
variants = [attrs]
|
|
314
|
+
for action in current._actions:
|
|
315
|
+
if isinstance(action, argparse._SubParsersAction) or action.choices is None:
|
|
316
|
+
continue
|
|
317
|
+
variants.extend({**attrs, action.dest: choice} for choice in action.choices)
|
|
318
|
+
for variant in variants:
|
|
319
|
+
route = cli_route_from_namespace(Namespace(**variant))
|
|
320
|
+
if route is None:
|
|
321
|
+
route = _normalize_cli_parser_path(path)
|
|
322
|
+
if route:
|
|
323
|
+
identity = f"{handler.__module__}:{handler.__qualname__}"
|
|
324
|
+
discovered.setdefault(route, set()).add(identity)
|
|
325
|
+
|
|
326
|
+
for action in current._actions:
|
|
327
|
+
if not isinstance(action, argparse._SubParsersAction):
|
|
328
|
+
continue
|
|
329
|
+
for name, child in action.choices.items():
|
|
330
|
+
next_attrs = dict(attrs)
|
|
331
|
+
next_attrs[action.dest] = name
|
|
332
|
+
walk(child, path + (name,), next_attrs)
|
|
333
|
+
|
|
334
|
+
walk(parser, (), {})
|
|
335
|
+
return {route: tuple(sorted(handlers)) for route, handlers in discovered.items()}
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def _normalize_cli_parser_path(path: tuple[str, ...]) -> tuple[str, ...]:
|
|
339
|
+
normalized = list(path)
|
|
340
|
+
if normalized and normalized[0] == "build":
|
|
341
|
+
normalized[0] = "builder"
|
|
342
|
+
if len(normalized) > 1 and normalized[0] == "builder" and normalized[1] == "associated-resources":
|
|
343
|
+
normalized[1] = "associated-resource"
|
|
344
|
+
return tuple(normalized)
|
|
@@ -2,8 +2,6 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from datetime import date
|
|
4
4
|
|
|
5
|
-
from mcp.server.fastmcp import FastMCP
|
|
6
|
-
|
|
7
5
|
from .backend_client import BackendClient
|
|
8
6
|
from .session_store import SessionStore
|
|
9
7
|
from .tools.app_tools import AppTools
|
|
@@ -26,9 +24,11 @@ from .tools.task_context_tools import TaskContextTools
|
|
|
26
24
|
from .tools.view_tools import ViewTools
|
|
27
25
|
from .tools.workflow_tools import WorkflowTools
|
|
28
26
|
from .tools.workspace_tools import WorkspaceTools
|
|
27
|
+
from .tool_catalog import stdio_server_bindings
|
|
28
|
+
from .tool_policy import PolicyAwareFastMCP, configure_tool_policy
|
|
29
29
|
|
|
30
30
|
|
|
31
|
-
def _register_missing_builder_tools(server:
|
|
31
|
+
def _register_missing_builder_tools(server: PolicyAwareFastMCP) -> None:
|
|
32
32
|
"""Add current high-level builder tools to the unified MCP without duplicate names."""
|
|
33
33
|
from .server_app_builder import build_builder_server
|
|
34
34
|
|
|
@@ -48,10 +48,10 @@ def _register_missing_builder_tools(server: FastMCP) -> None:
|
|
|
48
48
|
)
|
|
49
49
|
|
|
50
50
|
|
|
51
|
-
def build_server() ->
|
|
51
|
+
def build_server() -> PolicyAwareFastMCP:
|
|
52
52
|
today = date.today()
|
|
53
53
|
current_year = today.year
|
|
54
|
-
server =
|
|
54
|
+
server = PolicyAwareFastMCP(
|
|
55
55
|
"Qingflow MCP",
|
|
56
56
|
instructions=f"""Use this unified server for Qingflow operational and builder workflows. Current date: `{today.isoformat()}`.
|
|
57
57
|
|
|
@@ -259,8 +259,14 @@ If the current MCP capability is unsupported, the workflow is awkward, or the us
|
|
|
259
259
|
WorkflowTools(sessions, backend).register(server)
|
|
260
260
|
ViewTools(sessions, backend).register(server)
|
|
261
261
|
SolutionTools(sessions, backend).register(server)
|
|
262
|
+
# 保存补齐 Builder 前的真实注册来源,裸名冲突只能绑定最终保留 callable 的来源域。
|
|
263
|
+
legacy_tool_names = set(server._tool_manager._tools)
|
|
262
264
|
_register_missing_builder_tools(server)
|
|
263
|
-
return
|
|
265
|
+
return configure_tool_policy(
|
|
266
|
+
server,
|
|
267
|
+
stdio_server_bindings(legacy_tool_names, server._tool_manager._tools),
|
|
268
|
+
source="environment",
|
|
269
|
+
)
|
|
264
270
|
|
|
265
271
|
|
|
266
272
|
mcp = build_server()
|
|
@@ -2,8 +2,6 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
4
|
|
|
5
|
-
from mcp.server.fastmcp import FastMCP
|
|
6
|
-
|
|
7
5
|
from .backend_client import BackendClient
|
|
8
6
|
from .config import DEFAULT_PROFILE
|
|
9
7
|
from .response_trim import BUILDER_SERVER_METHOD_MAP, trim_error_response, trim_public_response, wrap_trimmed_methods
|
|
@@ -14,6 +12,8 @@ from .tools.auth_tools import AuthTools
|
|
|
14
12
|
from .tools.feedback_tools import FeedbackTools
|
|
15
13
|
from .tools.file_tools import FileTools
|
|
16
14
|
from .tools.workspace_tools import WorkspaceTools
|
|
15
|
+
from .tool_catalog import builder_server_bindings
|
|
16
|
+
from .tool_policy import PolicyAwareFastMCP, configure_tool_policy
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
def _config_failure(message: str, *, fix_hint: str) -> dict:
|
|
@@ -27,8 +27,8 @@ def _config_failure(message: str, *, fix_hint: str) -> dict:
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
|
|
30
|
-
def build_builder_server() ->
|
|
31
|
-
server =
|
|
30
|
+
def build_builder_server() -> PolicyAwareFastMCP:
|
|
31
|
+
server = PolicyAwareFastMCP(
|
|
32
32
|
"Qingflow App Builder MCP",
|
|
33
33
|
instructions=(
|
|
34
34
|
"Use this server for AI-native Qingflow builder workflows. "
|
|
@@ -689,7 +689,11 @@ def build_builder_server() -> FastMCP:
|
|
|
689
689
|
expected_package_id=expected_package_id,
|
|
690
690
|
)
|
|
691
691
|
|
|
692
|
-
return
|
|
692
|
+
return configure_tool_policy(
|
|
693
|
+
server,
|
|
694
|
+
builder_server_bindings(server._tool_manager._tools),
|
|
695
|
+
source="environment",
|
|
696
|
+
)
|
|
693
697
|
|
|
694
698
|
|
|
695
699
|
def main() -> None:
|
|
@@ -3,8 +3,6 @@ from __future__ import annotations
|
|
|
3
3
|
import json
|
|
4
4
|
from datetime import date
|
|
5
5
|
|
|
6
|
-
from mcp.server.fastmcp import FastMCP
|
|
7
|
-
|
|
8
6
|
from .backend_client import BackendClient
|
|
9
7
|
from .config import DEFAULT_PROFILE
|
|
10
8
|
from .response_trim import USER_SERVER_METHOD_MAP, trim_error_response, trim_public_response, wrap_trimmed_methods
|
|
@@ -22,12 +20,14 @@ from .tools.login_link_tools import LOGIN_LINK_DESCRIPTION, LoginLinkTools
|
|
|
22
20
|
from .tools.resource_read_tools import ResourceReadTools
|
|
23
21
|
from .tools.task_context_tools import TaskContextTools
|
|
24
22
|
from .tools.workspace_tools import WorkspaceTools
|
|
23
|
+
from .tool_catalog import user_server_bindings
|
|
24
|
+
from .tool_policy import PolicyAwareFastMCP, configure_tool_policy
|
|
25
25
|
|
|
26
26
|
|
|
27
|
-
def build_user_server(*, stateless_http: bool = False) ->
|
|
27
|
+
def build_user_server(*, stateless_http: bool = False) -> PolicyAwareFastMCP:
|
|
28
28
|
today = date.today()
|
|
29
29
|
current_year = today.year
|
|
30
|
-
server =
|
|
30
|
+
server = PolicyAwareFastMCP(
|
|
31
31
|
"Qingflow App User MCP",
|
|
32
32
|
stateless_http=stateless_http,
|
|
33
33
|
instructions=f"""Use this server for Qingflow operational workflows. Current date: `{today.isoformat()}`.
|
|
@@ -507,7 +507,11 @@ If the current MCP capability is unsupported, the workflow is awkward, or the us
|
|
|
507
507
|
task_context_tools.register(server)
|
|
508
508
|
directory_tools.register_frontend_search(server)
|
|
509
509
|
|
|
510
|
-
return
|
|
510
|
+
return configure_tool_policy(
|
|
511
|
+
server,
|
|
512
|
+
user_server_bindings(server._tool_manager._tools),
|
|
513
|
+
source="request" if stateless_http else "environment",
|
|
514
|
+
)
|
|
511
515
|
|
|
512
516
|
|
|
513
517
|
mcp = build_user_server()
|
|
@@ -6,11 +6,15 @@ from typing import Any
|
|
|
6
6
|
|
|
7
7
|
import anyio
|
|
8
8
|
import uvicorn
|
|
9
|
-
from mcp.server.fastmcp import FastMCP
|
|
10
|
-
|
|
11
9
|
from .backend_client import BackendClient
|
|
12
10
|
from .cloud_auth import CloudAuthBroker
|
|
13
|
-
from .cloud_context import
|
|
11
|
+
from .cloud_context import (
|
|
12
|
+
CloudMcpContext,
|
|
13
|
+
current_call_platform,
|
|
14
|
+
current_cloud_context,
|
|
15
|
+
current_http_tool_policy,
|
|
16
|
+
current_tool_policy_header,
|
|
17
|
+
)
|
|
14
18
|
from .cloud_oauth_proxy import (
|
|
15
19
|
CloudOAuthProxyConfig,
|
|
16
20
|
OAuthAccessTokenResolver,
|
|
@@ -21,6 +25,8 @@ from .config import get_cloud_http_allowed_hosts, get_cloud_http_host, get_cloud
|
|
|
21
25
|
from .errors import QingflowApiError, is_auth_like_error
|
|
22
26
|
from .server_app_builder import build_builder_server
|
|
23
27
|
from .server_app_user import build_user_server
|
|
28
|
+
from .tool_catalog import combined_server_bindings
|
|
29
|
+
from .tool_policy import PolicyAwareFastMCP, configure_tool_policy, parse_http_tool_policy
|
|
24
30
|
|
|
25
31
|
|
|
26
32
|
BUILDER_TOOL_PREFIX = "builder_"
|
|
@@ -164,12 +170,18 @@ class CloudMcpAuthApp:
|
|
|
164
170
|
cloud_context.token,
|
|
165
171
|
)
|
|
166
172
|
|
|
173
|
+
tool_policy_header = _extract_header(scope, "x-qingflow-tool-policy")
|
|
174
|
+
http_tool_policy = parse_http_tool_policy(tool_policy_header, bytes(scope.get("query_string") or b""))
|
|
167
175
|
context_token = current_cloud_context.set(cloud_context)
|
|
168
176
|
call_platform_token = current_call_platform.set(call_platform)
|
|
177
|
+
tool_policy_token = current_tool_policy_header.set(tool_policy_header)
|
|
178
|
+
http_tool_policy_token = current_http_tool_policy.set(http_tool_policy)
|
|
169
179
|
try:
|
|
170
180
|
scope = _ensure_streamable_http_accept(scope)
|
|
171
181
|
await self._app(scope, receive, send)
|
|
172
182
|
finally:
|
|
183
|
+
current_http_tool_policy.reset(http_tool_policy_token)
|
|
184
|
+
current_tool_policy_header.reset(tool_policy_token)
|
|
173
185
|
current_call_platform.reset(call_platform_token)
|
|
174
186
|
current_cloud_context.reset(context_token)
|
|
175
187
|
|
|
@@ -397,16 +409,29 @@ def build_cloud_app() -> CloudMcpAuthApp:
|
|
|
397
409
|
)
|
|
398
410
|
|
|
399
411
|
|
|
400
|
-
def build_combined_server() ->
|
|
412
|
+
def build_combined_server() -> PolicyAwareFastMCP:
|
|
401
413
|
server = build_user_server(stateless_http=True)
|
|
402
414
|
builder_server = build_builder_server()
|
|
415
|
+
user_tool_names = set(server._tool_manager._tools)
|
|
416
|
+
builder_tool_names = set(builder_server._tool_manager._tools)
|
|
403
417
|
server._mcp_server.name = "Qingflow Combined MCP"
|
|
404
418
|
_register_prefixed_builder_tools(server, builder_server)
|
|
405
419
|
_set_combined_instructions(server, builder_server)
|
|
406
|
-
return
|
|
420
|
+
return configure_tool_policy(
|
|
421
|
+
server,
|
|
422
|
+
combined_server_bindings(
|
|
423
|
+
user_tool_names,
|
|
424
|
+
builder_tool_names,
|
|
425
|
+
builder_prefix=BUILDER_TOOL_PREFIX,
|
|
426
|
+
),
|
|
427
|
+
source="request",
|
|
428
|
+
)
|
|
407
429
|
|
|
408
430
|
|
|
409
|
-
def _register_prefixed_builder_tools(
|
|
431
|
+
def _register_prefixed_builder_tools(
|
|
432
|
+
target_server: PolicyAwareFastMCP,
|
|
433
|
+
builder_server: PolicyAwareFastMCP,
|
|
434
|
+
) -> None:
|
|
410
435
|
for original_name, tool in builder_server._tool_manager._tools.items():
|
|
411
436
|
target_server._tool_manager.add_tool(
|
|
412
437
|
tool.fn,
|
|
@@ -419,7 +444,10 @@ def _register_prefixed_builder_tools(target_server: FastMCP, builder_server: Fas
|
|
|
419
444
|
)
|
|
420
445
|
|
|
421
446
|
|
|
422
|
-
def _set_combined_instructions(
|
|
447
|
+
def _set_combined_instructions(
|
|
448
|
+
user_server: PolicyAwareFastMCP,
|
|
449
|
+
builder_server: PolicyAwareFastMCP,
|
|
450
|
+
) -> None:
|
|
423
451
|
user_instructions = user_server.instructions or ""
|
|
424
452
|
builder_instructions = builder_server.instructions or ""
|
|
425
453
|
user_server._mcp_server.instructions = f"""Use this combined Qingflow MCP server for both user operational workflows and builder workflows.
|
|
@@ -454,6 +482,14 @@ def _extract_host(scope: dict[str, Any]) -> str:
|
|
|
454
482
|
return ""
|
|
455
483
|
|
|
456
484
|
|
|
485
|
+
def _extract_header(scope: dict[str, Any], name: str) -> str | None:
|
|
486
|
+
expected = name.lower().encode("latin1")
|
|
487
|
+
for key, value in scope.get("headers", []):
|
|
488
|
+
if key.lower() == expected:
|
|
489
|
+
return value.decode("utf-8", errors="replace")
|
|
490
|
+
return None
|
|
491
|
+
|
|
492
|
+
|
|
457
493
|
async def _read_request_body(
|
|
458
494
|
receive: Callable[[], Awaitable[dict[str, Any]]],
|
|
459
495
|
limit: int,
|