@qingflow-tech/qingflow-cli 1.1.60 → 1.1.61-beta.2
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 +3 -1
- package/skills/qingflow-cli/SKILL.md +120 -8
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_DATA_RETRIEVAL_WORKFLOW.md +1 -1
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_EXPLORATION_REPORT.md +1 -1
- package/skills/qingflow-cli/reference/builder/20-build-complete-system.md +2 -2
- package/skills/qingflow-cli/reference/builder/30-schema-fields.md +9 -5
- package/skills/qingflow-cli/reference/builder/reference/app-delivery-sop.md +3 -3
- package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_DATA_RETRIEVAL_WORKFLOW.md +4 -4
- package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_EXPLORATION_REPORT.md +1 -1
- package/skills/qingflow-cli/reference/data/README.md +20 -0
- package/skills/qingflow-cli/reference/record/QINGFLOW_CLI_RECORD_SPECIALIZED_WORKFLOW.md +7 -1
- package/skills/qingflow-common/SKILL.md +5 -2
- package/src/qingflow_mcp/app_form_compat.py +3 -0
- package/src/qingflow_mcp/app_form_validator.py +69 -17
- package/src/qingflow_mcp/backend_client.py +33 -8
- package/src/qingflow_mcp/builder_facade/app_form.py +497 -100
- package/src/qingflow_mcp/builder_facade/schemas/app_form/manifest.json +1 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/complete.json +64 -30
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/address.json +4 -8
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/amount.json +4 -8
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/attachment.json +6 -10
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/code_block.json +6 -10
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/data_relation.json +11 -18
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/date.json +4 -8
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/datetime.json +4 -8
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/department.json +6 -10
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/email.json +4 -8
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/link.json +4 -8
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/long_text.json +4 -8
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/member.json +6 -10
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/multi_select.json +4 -8
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/number.json +4 -8
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/phone.json +4 -8
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/q_linker.json +6 -10
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/relation.json +46 -15
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/rich_text.json +146 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/single_choice.json +4 -8
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/single_select.json +4 -8
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/subtable.json +61 -27
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/text.json +4 -8
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/formula.json +43 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/summary.json +7 -12
- package/src/qingflow_mcp/builder_facade/service.py +39 -1
- package/src/qingflow_mcp/cli/commands/data.py +13 -3
- package/src/qingflow_mcp/cli/formatters.py +7 -0
- 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/response_trim.py +2 -1
- package/src/qingflow_mcp/server.py +13 -7
- package/src/qingflow_mcp/server_app_builder.py +9 -5
- package/src/qingflow_mcp/server_app_user.py +10 -6
- 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/src/qingflow_mcp/tools/data_files.py +174 -0
- package/src/qingflow_mcp/tools/data_tools.py +42 -13
- package/src/qingflow_mcp/tools/record_tools.py +14 -72
- package/src/qingflow_mcp/tools/storage_download.py +89 -0
|
@@ -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)
|
|
@@ -51,8 +51,9 @@ COMMON_ERROR_DROP_TOP = {
|
|
|
51
51
|
|
|
52
52
|
SUCCESS_POLICY_BY_TOOL: dict[str, TransformFn] = {}
|
|
53
53
|
|
|
54
|
-
#:
|
|
54
|
+
#: 这些校验命令的产出契约明确包含 ok 字段(供阶段 2 SKILL.md 引用),
|
|
55
55
|
#: 需在 success 裁剪时保留 ok(默认 COMMON_SUCCESS_DROP_TOP 会丢弃 ok)。
|
|
56
|
+
#: `builder:app_views_validate`(views 声明式校验)同属校验类工具,ok 同样进产出契约。
|
|
56
57
|
_KEEP_OK_TOOLS = {
|
|
57
58
|
"builder:app_form_validate_formula",
|
|
58
59
|
"builder:app_form_validate_code_block",
|
|
@@ -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
|
|
|
@@ -149,7 +149,7 @@ Analysis answers must include concrete numbers. When applicable, include percent
|
|
|
149
149
|
- `record_delete` deletes by `record_id` or `record_ids`, and requires an accessible system `view_id`; use custom views only to locate records, not as the delete route.
|
|
150
150
|
- `record_get` is the single-record frontend detail context tool. It returns detail-page visible fields, one-level relation targets, first-page data/workflow logs, associated views/reports, local readable image assets, local downloadable file assets, unavailable context, and `semantic_context`.
|
|
151
151
|
- Use `record_logs_get` only when the user needs the full visible data/workflow log history for a specific record. It writes JSONL files locally and returns file paths plus completeness metadata; do not expect full log arrays in the response.
|
|
152
|
-
-
|
|
152
|
+
- Download record attachments with `data_get(download_attachments=true, include_schema=true)` for the same app and record; use `file_assets.items[].local_path` only when `access_status=downloaded`. This includes original images and videos. Inspect `file_assets.warnings` for partial failures; do not request remote private URLs or fall back to record/detail tools after a data-plane denial. For specialized detail-page context, read `record_get` rich-text images from `media_assets` and its files from `file_assets` local paths or `extraction.text_path` when present.
|
|
153
153
|
- `record_get.columns` are focus hints only; they do not project the detail fields. Read facts from top-level `fields[]`.
|
|
154
154
|
- When readback shape matters after insert or update, prefer `record_get` for human/detail-page context, or `record_list(..., output_profile="normalized")` for batch-shaped normalized rows.
|
|
155
155
|
|
|
@@ -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()}`.
|
|
@@ -144,7 +144,7 @@ Analysis answers must include concrete numbers. When applicable, include percent
|
|
|
144
144
|
- `record_delete` deletes by `record_id` or `record_ids`, and requires an accessible system `view_id`; use custom views only to locate records, not as the delete route.
|
|
145
145
|
- `record_get` is the single-record frontend detail context tool. It returns detail-page visible fields, one-level relation targets, first-page data/workflow logs, associated views/reports, local readable image assets, local downloadable file assets, unavailable context, and `semantic_context`.
|
|
146
146
|
- Use `record_logs_get` only when the user needs the full visible data/workflow log history for a specific record. It writes JSONL files locally and returns file paths plus completeness metadata; do not expect full log arrays in the response.
|
|
147
|
-
-
|
|
147
|
+
- Download record attachments with `data_get(download_attachments=true, include_schema=true)` for the same app and record; use `file_assets.items[].local_path` only when `access_status=downloaded`. This includes original images and videos. Inspect `file_assets.warnings` for partial failures; do not request remote private URLs or fall back to record/detail tools after a data-plane denial. For specialized detail-page context, read `record_get` rich-text images from `media_assets` and its files from `file_assets` local paths or `extraction.text_path` when present.
|
|
148
148
|
- `record_get.columns` are focus hints only; they do not project the detail fields. Read facts from top-level `fields[]`.
|
|
149
149
|
- When readback shape matters after insert or update, prefer `record_get` for human/detail-page context, or `record_list(..., output_profile="normalized")` for batch-shaped normalized rows.
|
|
150
150
|
|
|
@@ -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,
|