@qingflow-tech/qingflow-cli 1.1.48 → 1.1.50
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 +4 -4
- package/npm/lib/runtime.mjs +29 -13
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/skills/qingflow-cli/SKILL.md +91 -410
- package/skills/qingflow-cli/manifest.yaml +1 -1
- package/skills/qingflow-cli/reference/00-INDEX.md +4 -4
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_ADMIN_CHEATSHEET.md +1 -1
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_DATA_RETRIEVAL_WORKFLOW.md +3 -137
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_EXPLORATION_REPORT.md +6 -4
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_FIELD_DATA_TYPES.md +14 -14
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_MEMBER_CHEATSHEET.md +20 -19
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_ONE_SHOT_CHEATSHEET.md +18 -14
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_RECORD_CREATE_WORKFLOW.md +7 -12
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_RECORD_UPDATE_WORKFLOW.md +2 -1
- package/skills/qingflow-cli/reference/builder/workflow/03-flow-patterns.md +13 -5
- package/skills/qingflow-cli/reference/builder/workflow/07-stage4-validate-spec.md +5 -3
- package/skills/qingflow-cli/reference/builder/workflow/08-stage5-apply-verify.md +1 -1
- package/skills/qingflow-cli/reference/builder/workflow/11-troubleshooting.md +1 -1
- package/skills/qingflow-cli/reference/builder/workflow/workflow-schema.json +1665 -1738
- package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_ADMIN_CHEATSHEET.md +1 -1
- package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_DATA_RETRIEVAL_WORKFLOW.md +76 -95
- package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_EXPLORATION_REPORT.md +6 -4
- package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_FIELD_DATA_TYPES.md +13 -13
- package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_MEMBER_CHEATSHEET.md +20 -19
- package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_ONE_SHOT_CHEATSHEET.md +18 -14
- package/skills/qingflow-cli/reference/data/README.md +32 -0
- package/skills/qingflow-cli/reference/record/QINGFLOW_CLI_RECORD_SPECIALIZED_WORKFLOW.md +50 -0
- package/skills/qingflow-cli/reference/record/QINGFLOW_CLI_RECORD_UPDATE_WORKFLOW.md +2 -1
- package/skills/qingflow-cli/reference/record/analysis/README.md +21 -113
- package/skills/qingflow-cli/reference/record/analysis/analysis-gotchas.md +10 -10
- package/skills/qingflow-cli/reference/record/analysis/analysis-patterns.md +9 -9
- package/skills/qingflow-cli/reference/record/analysis/business-context.md +1 -1
- package/skills/qingflow-cli/reference/record/analysis/confidence-reporting.md +10 -10
- package/skills/qingflow-cli/reference/record/analysis/data-access-playbook.md +52 -80
- package/skills/qingflow-cli/reference/record/analysis/pandas-recipes.md +2 -2
- package/skills/qingflow-cli/scripts/workflow/validate_flow_spec.py +48 -46
- package/skills/qingflow-common/SKILL.md +10 -17
- package/src/qingflow_mcp/__init__.py +1 -1
- package/src/qingflow_mcp/backend_client.py +142 -12
- package/src/qingflow_mcp/builder_facade/flow_validation.py +569 -0
- package/src/qingflow_mcp/builder_facade/service.py +762 -6
- package/src/qingflow_mcp/builder_facade/workflow_spec.py +155 -3
- package/src/qingflow_mcp/cli/commands/__init__.py +2 -1
- package/src/qingflow_mcp/cli/commands/auth.py +1 -1
- package/src/qingflow_mcp/cli/commands/builder.py +57 -41
- package/src/qingflow_mcp/cli/commands/data.py +134 -0
- package/src/qingflow_mcp/cli/commands/record.py +1 -1
- package/src/qingflow_mcp/cli/commands/task.py +10 -12
- package/src/qingflow_mcp/cli/commands/workspace.py +20 -7
- package/src/qingflow_mcp/cli/context.py +4 -1
- package/src/qingflow_mcp/cli/formatters.py +177 -21
- package/src/qingflow_mcp/cli/main.py +4 -1
- package/src/qingflow_mcp/cli/pagination.py +44 -0
- package/src/qingflow_mcp/config.py +60 -0
- package/src/qingflow_mcp/public_surface.py +9 -0
- package/src/qingflow_mcp/resource_uri.py +241 -0
- package/src/qingflow_mcp/response_trim.py +56 -5
- package/src/qingflow_mcp/server.py +2 -0
- package/src/qingflow_mcp/server_app_builder.py +57 -2
- package/src/qingflow_mcp/server_app_user.py +10 -3
- package/src/qingflow_mcp/tools/ai_builder_tools.py +374 -41
- package/src/qingflow_mcp/tools/app_tools.py +52 -13
- package/src/qingflow_mcp/tools/csv_export.py +168 -0
- package/src/qingflow_mcp/tools/data_tools.py +284 -0
- package/src/qingflow_mcp/tools/login_link_tools.py +34 -6
- package/src/qingflow_mcp/tools/record_tools.py +85 -61
- package/src/qingflow_mcp/tools/resource_read_tools.py +13 -0
- package/src/qingflow_mcp/tools/task_context_tools.py +21 -4
- package/src/qingflow_mcp/tools/workspace_tools.py +9 -12
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from collections.abc import Iterator
|
|
5
|
+
from contextlib import contextmanager
|
|
6
|
+
from contextvars import ContextVar
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from typing import Any
|
|
9
|
+
from urllib.parse import quote, unquote, urlsplit, urlunsplit
|
|
10
|
+
|
|
11
|
+
from .config import DEFAULT_WEB_BASE_URL, get_web_base_url
|
|
12
|
+
from .errors import QingflowApiError
|
|
13
|
+
from .json_types import JSONObject
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
SYSTEM_VIEW_NAMES = frozenset({"all", "initiated", "todo", "done", "cc"})
|
|
17
|
+
_ENCODED_OCTET = re.compile(r"%[0-9A-Fa-f]{2}")
|
|
18
|
+
_CLI_RESOURCE_BASE_URL: ContextVar[str | None] = ContextVar(
|
|
19
|
+
"qingflow_cli_resource_base_url",
|
|
20
|
+
default=None,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True, slots=True)
|
|
25
|
+
class ResourcePath:
|
|
26
|
+
resource_type: str
|
|
27
|
+
ws_id: int
|
|
28
|
+
package_id: int | None = None
|
|
29
|
+
app_key: str | None = None
|
|
30
|
+
view_kind: str | None = None
|
|
31
|
+
view_key: str | None = None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@contextmanager
|
|
35
|
+
def cli_resource_base_url(value: str) -> Iterator[None]:
|
|
36
|
+
"""让一次 CLI 调用的资源 URI 使用 QINGFLOW_BASE_URL 对应的网页环境。"""
|
|
37
|
+
token = _CLI_RESOURCE_BASE_URL.set(value)
|
|
38
|
+
try:
|
|
39
|
+
yield
|
|
40
|
+
finally:
|
|
41
|
+
_CLI_RESOURCE_BASE_URL.reset(token)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def validate_resource_identity(value: object, field: str = "resource_id") -> str:
|
|
45
|
+
"""按资源 URI 单路径段合同校验并返回规范身份,不读取网页域名配置。"""
|
|
46
|
+
return _validate_identity(value, field)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def validate_positive_resource_id(value: object, field: str = "resource_id") -> int:
|
|
50
|
+
"""按资源 URI 数字路径段合同校验正整数,不接受布尔值或浮点截断。"""
|
|
51
|
+
return int(_positive_int_segment(value, field))
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def build_resource_uri(
|
|
55
|
+
*,
|
|
56
|
+
ws_id: object,
|
|
57
|
+
resource_type: str,
|
|
58
|
+
package_id: object = None,
|
|
59
|
+
app_key: object = None,
|
|
60
|
+
view_kind: str | None = None,
|
|
61
|
+
view_key: object = None,
|
|
62
|
+
web_origin: str | None = None,
|
|
63
|
+
) -> str:
|
|
64
|
+
"""按固定公共合同构造资源 URI,不接受通用目标路由。"""
|
|
65
|
+
workspace = _positive_int_segment(ws_id, "ws_id")
|
|
66
|
+
if resource_type == "package":
|
|
67
|
+
path = f"/resources/ws/{workspace}/package/{_positive_int_segment(package_id, 'package_id')}"
|
|
68
|
+
elif resource_type == "app":
|
|
69
|
+
path = f"/resources/ws/{workspace}/app/{_identity_segment(app_key, 'app_key')}"
|
|
70
|
+
elif resource_type == "view":
|
|
71
|
+
app_segment = _identity_segment(app_key, "app_key")
|
|
72
|
+
if view_kind == "custom":
|
|
73
|
+
view_segment = _identity_segment(view_key, "view_key")
|
|
74
|
+
elif view_kind == "system":
|
|
75
|
+
name = str(view_key or "").strip()
|
|
76
|
+
if name not in SYSTEM_VIEW_NAMES:
|
|
77
|
+
raise ValueError("system view name is not supported")
|
|
78
|
+
view_segment = name
|
|
79
|
+
else:
|
|
80
|
+
raise ValueError("view_kind must be custom or system")
|
|
81
|
+
path = f"/resources/ws/{workspace}/app/{app_segment}/view/{view_kind}/{view_segment}"
|
|
82
|
+
else:
|
|
83
|
+
raise ValueError("resource_type is not supported")
|
|
84
|
+
return f"{resolve_resource_web_origin(web_origin)}{path}"
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def parse_resource_path(value: str) -> ResourcePath:
|
|
88
|
+
"""严格解析资源 pathname,供免登录签发前校验工作区。"""
|
|
89
|
+
try:
|
|
90
|
+
parsed = urlsplit(value)
|
|
91
|
+
if parsed.scheme or parsed.netloc or parsed.query or parsed.fragment or parsed.path != value:
|
|
92
|
+
raise ValueError
|
|
93
|
+
parts = parsed.path.split("/")
|
|
94
|
+
if len(parts) < 6 or parts[:3] != ["", "resources", "ws"]:
|
|
95
|
+
raise ValueError
|
|
96
|
+
ws_id = int(_decode_segment(parts[3], "ws_id"))
|
|
97
|
+
if ws_id <= 0:
|
|
98
|
+
raise ValueError
|
|
99
|
+
if len(parts) == 6 and parts[4] == "package":
|
|
100
|
+
package_id = int(_decode_segment(parts[5], "package_id"))
|
|
101
|
+
if package_id <= 0:
|
|
102
|
+
raise ValueError
|
|
103
|
+
return ResourcePath("package", ws_id, package_id=package_id)
|
|
104
|
+
if len(parts) == 6 and parts[4] == "app":
|
|
105
|
+
return ResourcePath("app", ws_id, app_key=_decode_segment(parts[5], "app_key"))
|
|
106
|
+
if len(parts) == 9 and parts[4] == "app" and parts[6] == "view":
|
|
107
|
+
app_key = _decode_segment(parts[5], "app_key")
|
|
108
|
+
view_kind = parts[7]
|
|
109
|
+
view_key = _decode_segment(parts[8], "view_key")
|
|
110
|
+
if view_kind == "system" and view_key not in SYSTEM_VIEW_NAMES:
|
|
111
|
+
raise ValueError
|
|
112
|
+
if view_kind not in {"custom", "system"}:
|
|
113
|
+
raise ValueError
|
|
114
|
+
return ResourcePath("view", ws_id, app_key=app_key, view_kind=view_kind, view_key=view_key)
|
|
115
|
+
except (TypeError, ValueError, UnicodeError):
|
|
116
|
+
pass
|
|
117
|
+
raise ValueError("resource path is invalid")
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def try_build_resource_uri(**kwargs: Any) -> tuple[str | None, JSONObject | None]:
|
|
121
|
+
try:
|
|
122
|
+
return build_resource_uri(**kwargs), None
|
|
123
|
+
except (QingflowApiError, ValueError, UnicodeError) as error:
|
|
124
|
+
return None, {
|
|
125
|
+
"code": "RESOURCE_URI_UNAVAILABLE",
|
|
126
|
+
"message": f"resource URI was omitted because its identity or web origin is invalid: {error}",
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _positive_int_segment(value: object, field: str) -> str:
|
|
131
|
+
if isinstance(value, bool):
|
|
132
|
+
raise ValueError(f"{field} must be a positive integer")
|
|
133
|
+
try:
|
|
134
|
+
parsed = int(value)
|
|
135
|
+
except (TypeError, ValueError):
|
|
136
|
+
raise ValueError(f"{field} must be a positive integer") from None
|
|
137
|
+
if parsed <= 0 or str(value).strip() != str(parsed):
|
|
138
|
+
raise ValueError(f"{field} must be a positive integer")
|
|
139
|
+
return str(parsed)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _normalize_web_origin(value: str) -> str:
|
|
143
|
+
try:
|
|
144
|
+
parsed = urlsplit(value.strip())
|
|
145
|
+
if (
|
|
146
|
+
parsed.scheme not in {"http", "https"}
|
|
147
|
+
or not parsed.hostname
|
|
148
|
+
or parsed.username is not None
|
|
149
|
+
or parsed.password is not None
|
|
150
|
+
or parsed.path not in {"", "/"}
|
|
151
|
+
or parsed.query
|
|
152
|
+
or parsed.fragment
|
|
153
|
+
or any(character.isspace() or ord(character) < 32 for character in value)
|
|
154
|
+
):
|
|
155
|
+
raise ValueError
|
|
156
|
+
_ = parsed.port
|
|
157
|
+
except (AttributeError, ValueError):
|
|
158
|
+
raise ValueError("web_origin must be an absolute HTTP(S) origin") from None
|
|
159
|
+
return urlunsplit((parsed.scheme, parsed.netloc, "", "", ""))
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def resolve_resource_web_origin(web_origin: str | None = None) -> str:
|
|
163
|
+
"""解析当前调用面的网页根地址;CLI 与 MCP 分别沿用各自的环境配置。"""
|
|
164
|
+
if web_origin is not None:
|
|
165
|
+
return _normalize_web_origin(web_origin)
|
|
166
|
+
cli_base_url = _CLI_RESOURCE_BASE_URL.get()
|
|
167
|
+
if cli_base_url is None:
|
|
168
|
+
return get_web_base_url()
|
|
169
|
+
return _web_origin_from_api_base_url(cli_base_url)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _web_origin_from_api_base_url(value: str) -> str:
|
|
173
|
+
normalized = value.strip()
|
|
174
|
+
if not normalized:
|
|
175
|
+
return DEFAULT_WEB_BASE_URL
|
|
176
|
+
try:
|
|
177
|
+
parsed = urlsplit(normalized)
|
|
178
|
+
host = parsed.hostname or ""
|
|
179
|
+
valid_host = bool(host) and (":" in host or all(
|
|
180
|
+
re.fullmatch(r"[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?", label)
|
|
181
|
+
for label in host.rstrip(".").split(".")
|
|
182
|
+
))
|
|
183
|
+
if (
|
|
184
|
+
parsed.scheme not in {"http", "https"}
|
|
185
|
+
or not valid_host
|
|
186
|
+
or host.lower() in {"null", "undefined"}
|
|
187
|
+
or parsed.username is not None
|
|
188
|
+
or parsed.password is not None
|
|
189
|
+
or parsed.query
|
|
190
|
+
or parsed.fragment
|
|
191
|
+
or any(character.isspace() or ord(character) < 32 for character in normalized)
|
|
192
|
+
):
|
|
193
|
+
raise ValueError
|
|
194
|
+
_ = parsed.port
|
|
195
|
+
return _normalize_web_origin(urlunsplit((parsed.scheme, parsed.netloc, "", "", "")))
|
|
196
|
+
except ValueError:
|
|
197
|
+
raise QingflowApiError.config_error(
|
|
198
|
+
"QINGFLOW_BASE_URL 必须是有效的 HTTP(S) API 地址,不包含查询参数、片段或用户凭证"
|
|
199
|
+
) from None
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _identity_segment(value: object, field: str) -> str:
|
|
203
|
+
raw = _validate_identity(value, field)
|
|
204
|
+
return quote(raw, safe="")
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _decode_segment(value: str, field: str) -> str:
|
|
208
|
+
decoded = _validate_identity(unquote(value, errors="strict"), field)
|
|
209
|
+
if quote(decoded, safe="") != value:
|
|
210
|
+
raise ValueError(f"{field} must use canonical path encoding")
|
|
211
|
+
return decoded
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _validate_identity(value: object, field: str) -> str:
|
|
215
|
+
source = str(value or "")
|
|
216
|
+
if any(ord(character) < 32 or ord(character) == 127 for character in source):
|
|
217
|
+
raise ValueError(f"{field} must be one safe path segment")
|
|
218
|
+
raw = source.strip()
|
|
219
|
+
if not raw:
|
|
220
|
+
raise ValueError(f"{field} is required")
|
|
221
|
+
current = raw
|
|
222
|
+
for _ in range(8):
|
|
223
|
+
if (
|
|
224
|
+
current in {".", ".."}
|
|
225
|
+
or any(character in current for character in "/\\?#")
|
|
226
|
+
or any(ord(character) < 32 or ord(character) == 127 for character in current)
|
|
227
|
+
):
|
|
228
|
+
raise ValueError(f"{field} must be one safe path segment")
|
|
229
|
+
decoded = unquote(current, errors="strict")
|
|
230
|
+
if decoded == current:
|
|
231
|
+
return raw
|
|
232
|
+
current = decoded
|
|
233
|
+
if (
|
|
234
|
+
current in {".", ".."}
|
|
235
|
+
or any(character in current for character in "/\\?#")
|
|
236
|
+
or any(ord(character) < 32 or ord(character) == 127 for character in current)
|
|
237
|
+
):
|
|
238
|
+
raise ValueError(f"{field} must be one safe path segment")
|
|
239
|
+
if _ENCODED_OCTET.search(current):
|
|
240
|
+
raise ValueError(f"{field} has excessive encoding")
|
|
241
|
+
return raw
|
|
@@ -53,7 +53,7 @@ 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
|
-
_KEEP_OK_TOOLS = {"builder:app_form_validate_formula", "builder:app_form_validate_code_block"}
|
|
56
|
+
_KEEP_OK_TOOLS = {"builder:app_form_validate_formula", "builder:app_form_validate_code_block", "builder:app_flow_validate"}
|
|
57
57
|
|
|
58
58
|
|
|
59
59
|
def trim_public_response(tool_name: str | None, payload: dict[str, Any]) -> dict[str, Any]:
|
|
@@ -313,6 +313,9 @@ def _trim_workspace_list(payload: JSONObject) -> None:
|
|
|
313
313
|
page = payload.get("page")
|
|
314
314
|
if isinstance(page, dict):
|
|
315
315
|
_trim_item_list(page, "list", allowed=("wsId", "workspaceName", "remark"))
|
|
316
|
+
selected = payload.get("selected_workspace")
|
|
317
|
+
if isinstance(selected, dict):
|
|
318
|
+
payload["selected_workspace"] = _pick(selected, allowed=("wsId", "workspaceName", "remark"))
|
|
316
319
|
|
|
317
320
|
|
|
318
321
|
def _trim_workspace_get(payload: JSONObject) -> None:
|
|
@@ -326,7 +329,7 @@ def _trim_workspace_get(payload: JSONObject) -> None:
|
|
|
326
329
|
|
|
327
330
|
def _trim_app_list_like(payload: JSONObject) -> None:
|
|
328
331
|
payload.pop("apps", None)
|
|
329
|
-
_trim_item_list(payload, "items", allowed=("app_key", "app_name", "package_name"))
|
|
332
|
+
_trim_item_list(payload, "items", allowed=("app_key", "app_name", "package_name", "uri"))
|
|
330
333
|
|
|
331
334
|
|
|
332
335
|
def _trim_app_get(payload: JSONObject) -> None:
|
|
@@ -334,7 +337,7 @@ def _trim_app_get(payload: JSONObject) -> None:
|
|
|
334
337
|
_trim_nested_item_list(
|
|
335
338
|
payload,
|
|
336
339
|
("data", "accessible_views"),
|
|
337
|
-
allowed=("view_id", "name", "analysis_supported", "list_supported", "kind"),
|
|
340
|
+
allowed=("view_id", "name", "analysis_supported", "list_supported", "kind", "uri"),
|
|
338
341
|
)
|
|
339
342
|
|
|
340
343
|
|
|
@@ -1107,6 +1110,51 @@ def _trim_app_form_apply(payload: JSONObject) -> None:
|
|
|
1107
1110
|
payload.setdefault("details", {})["diff"] = _summarize_form_diff(details_diff)
|
|
1108
1111
|
|
|
1109
1112
|
|
|
1113
|
+
#: app_flow_apply 的 semanticLint 逐条放行上限(#162 D6):diffSummary 是
|
|
1114
|
+
#: 「后端认为改了什么」的唯一依据、semanticLint 是驳回原因,两者是 agent
|
|
1115
|
+
#: 闭环修正的输入;超过 cap 加 semantic_lint_truncated 标记。
|
|
1116
|
+
FLOW_APPLY_SEMANTIC_LINT_LIMIT = 20
|
|
1117
|
+
|
|
1118
|
+
|
|
1119
|
+
def _trim_flow_apply(payload: JSONObject) -> None:
|
|
1120
|
+
"""app_flow_apply 专属裁剪:diffSummary 全量保留、semanticLint 保留前
|
|
1121
|
+
FLOW_APPLY_SEMANTIC_LINT 条(每条只留 code/message/severity/path);
|
|
1122
|
+
appliedSpec/apply_result 大对象仍被通用裁剪(verification 布尔已足够)。
|
|
1123
|
+
|
|
1124
|
+
必须在 _trim_builder_envelope 压缩 details 前先摘出关键明细(通用裁剪
|
|
1125
|
+
会把 list 形明细全裁),调用后回填。"""
|
|
1126
|
+
details = payload.get("details")
|
|
1127
|
+
diff_summary = details.get("diffSummary") if isinstance(details, dict) else None
|
|
1128
|
+
semantic_lint = details.get("semanticLint") if isinstance(details, dict) else None
|
|
1129
|
+
_trim_builder_envelope(payload)
|
|
1130
|
+
preserved: JSONObject = {}
|
|
1131
|
+
if isinstance(diff_summary, dict) and diff_summary:
|
|
1132
|
+
preserved["diffSummary"] = diff_summary
|
|
1133
|
+
if isinstance(semantic_lint, list):
|
|
1134
|
+
projected = [
|
|
1135
|
+
{
|
|
1136
|
+
"code": item.get("code"),
|
|
1137
|
+
"message": item.get("message"),
|
|
1138
|
+
"severity": item.get("severity"),
|
|
1139
|
+
"path": item.get("path"),
|
|
1140
|
+
}
|
|
1141
|
+
for item in semantic_lint
|
|
1142
|
+
if isinstance(item, dict)
|
|
1143
|
+
]
|
|
1144
|
+
if projected:
|
|
1145
|
+
if len(projected) > FLOW_APPLY_SEMANTIC_LINT_LIMIT:
|
|
1146
|
+
preserved["semanticLint"] = projected[:FLOW_APPLY_SEMANTIC_LINT_LIMIT]
|
|
1147
|
+
preserved["semantic_lint_truncated"] = True
|
|
1148
|
+
else:
|
|
1149
|
+
preserved["semanticLint"] = projected
|
|
1150
|
+
if preserved:
|
|
1151
|
+
merged = payload.get("details")
|
|
1152
|
+
if not isinstance(merged, dict):
|
|
1153
|
+
merged = {}
|
|
1154
|
+
payload["details"] = merged
|
|
1155
|
+
merged.update(preserved)
|
|
1156
|
+
|
|
1157
|
+
|
|
1110
1158
|
def _trim_form_diff(payload: JSONObject) -> None:
|
|
1111
1159
|
diff = payload.get("diff")
|
|
1112
1160
|
if isinstance(diff, dict):
|
|
@@ -1148,7 +1196,6 @@ def _register_policy(domains: tuple[str, ...], names: tuple[str, ...], transform
|
|
|
1148
1196
|
|
|
1149
1197
|
|
|
1150
1198
|
_register_policy((USER_DOMAIN, BUILDER_DOMAIN), ("auth_use_credential", "auth_whoami"), _trim_auth_payload)
|
|
1151
|
-
_register_policy((USER_DOMAIN,), ("get_login_link",), _trim_login_link)
|
|
1152
1199
|
_register_policy((USER_DOMAIN, BUILDER_DOMAIN), ("auth_logout",), _trim_auth_logout)
|
|
1153
1200
|
_register_policy((USER_DOMAIN, BUILDER_DOMAIN), ("workspace_list",), _trim_workspace_list)
|
|
1154
1201
|
_register_policy((USER_DOMAIN, BUILDER_DOMAIN), ("workspace_get", "workspace_select"), _trim_workspace_get)
|
|
@@ -1223,6 +1270,7 @@ _register_policy(
|
|
|
1223
1270
|
_trim_directory,
|
|
1224
1271
|
)
|
|
1225
1272
|
_register_policy((USER_DOMAIN, BUILDER_DOMAIN), ("feedback_submit",), _trim_feedback)
|
|
1273
|
+
_register_policy((USER_DOMAIN, BUILDER_DOMAIN), ("get_login_link",), _trim_login_link)
|
|
1226
1274
|
_register_policy(
|
|
1227
1275
|
(USER_DOMAIN,),
|
|
1228
1276
|
(
|
|
@@ -1259,12 +1307,14 @@ _register_policy(
|
|
|
1259
1307
|
"app_get_flow",
|
|
1260
1308
|
"app_flow_get_schema",
|
|
1261
1309
|
"app_flow_get",
|
|
1310
|
+
"app_flow_validate",
|
|
1311
|
+
"app_flow_list_versions",
|
|
1312
|
+
"app_flow_restore_version",
|
|
1262
1313
|
"app_get_charts",
|
|
1263
1314
|
"app_get_buttons",
|
|
1264
1315
|
"app_get_associated_resources",
|
|
1265
1316
|
"app_schema_apply",
|
|
1266
1317
|
"app_layout_apply",
|
|
1267
|
-
"app_flow_apply",
|
|
1268
1318
|
"app_views_apply",
|
|
1269
1319
|
"app_charts_apply",
|
|
1270
1320
|
"portal_apply",
|
|
@@ -1274,3 +1324,4 @@ _register_policy(
|
|
|
1274
1324
|
_trim_builder_list_like,
|
|
1275
1325
|
)
|
|
1276
1326
|
_register_policy((BUILDER_DOMAIN,), ("app_form_apply",), _trim_app_form_apply)
|
|
1327
|
+
_register_policy((BUILDER_DOMAIN,), ("app_flow_apply",), _trim_flow_apply)
|
|
@@ -7,6 +7,7 @@ from mcp.server.fastmcp import FastMCP
|
|
|
7
7
|
from .backend_client import BackendClient
|
|
8
8
|
from .session_store import SessionStore
|
|
9
9
|
from .tools.app_tools import AppTools
|
|
10
|
+
from .tools.data_tools import DataTools
|
|
10
11
|
from .tools.auth_tools import AuthTools
|
|
11
12
|
from .tools.code_block_tools import CodeBlockTools
|
|
12
13
|
from .tools.feedback_tools import FeedbackTools
|
|
@@ -249,6 +250,7 @@ If the current MCP capability is unsupported, the workflow is awkward, or the us
|
|
|
249
250
|
TaskContextTools(sessions, backend).register(server)
|
|
250
251
|
RoleTools(sessions, backend).register(server)
|
|
251
252
|
AppTools(sessions, backend).register(server)
|
|
253
|
+
DataTools(sessions, backend).register(server)
|
|
252
254
|
QingbiReportTools(sessions, backend).register(server)
|
|
253
255
|
PackageTools(sessions, backend).register(server)
|
|
254
256
|
NavigationTools(sessions, backend).register(server)
|
|
@@ -9,6 +9,7 @@ from .config import DEFAULT_PROFILE
|
|
|
9
9
|
from .response_trim import BUILDER_SERVER_METHOD_MAP, trim_error_response, trim_public_response, wrap_trimmed_methods
|
|
10
10
|
from .session_store import SessionStore
|
|
11
11
|
from .tools.ai_builder_tools import AiBuilderTools
|
|
12
|
+
from .tools.login_link_tools import LOGIN_LINK_DESCRIPTION, LoginLinkTools
|
|
12
13
|
from .tools.auth_tools import AuthTools
|
|
13
14
|
from .tools.feedback_tools import FeedbackTools
|
|
14
15
|
from .tools.file_tools import FileTools
|
|
@@ -39,8 +40,18 @@ def build_builder_server() -> FastMCP:
|
|
|
39
40
|
"Use workspace_icon_catalog_get before creating app packages or portals when supported icon/color candidates are needed; those resources require explicit non-template icon + color. AppForm-created apps derive a stable default icon and color from spec.appName. "
|
|
40
41
|
"app_form_schema plus app_form_get for versioned form work; query app_form_schema with schema_kind=formula before writing defaultType=3 formulas, and use being_draft=true only for editing and the published default for relation or record consumers; use app_get as the default non-form app map read, then app_get_fields/app_repair_code_blocks/app_get_layout/app_get_views/app_get_flow/app_get_charts/portal_list/portal_get/view_get/chart_get for focused compatibility or non-form reads, "
|
|
41
42
|
"member_search/role_search/role_create when workflow assignees must come from the directory or role catalog, preferring roles over explicit members unless the user explicitly names members, "
|
|
42
|
-
"then app_form_apply for complete app form targets, explicit app_delete for application deletion, and app_flow_apply/app_views_apply/app_associated_resources_apply/app_charts_apply/portal_apply/portal_delete for non-form patches. app_custom_buttons_apply is maintenance-only for explicit standalone button work outside normal view design. These apply/delete tools perform validation, normalization, writes, and readback verification where applicable. AppForm publishes only after a draft change or when the published form lags behind; views noop requests skip publish, app_custom_buttons_apply and app_associated_resources_apply publish after at least one write succeeds and expose no draft-only parameter, charts are immediate-live without publish and resolve targets by chart_id first then exact unique chart name, portal updates use replace semantics only when sections are supplied and edit-mode base-info-only updates may omit sections, portal delete separates DELETE execution from readback verification, portal pc layout is a 24-column grid and mobile is a 6-column grid so omit position or use layout_preset when unsure, publish=false only guarantees draft/base-info updates for tools that still expose that parameter, and flow should use publish=false
|
|
43
|
+
"then app_form_apply for complete app form targets, explicit app_delete for application deletion, and app_flow_apply/app_views_apply/app_associated_resources_apply/app_charts_apply/portal_apply/portal_delete for non-form patches. app_custom_buttons_apply is maintenance-only for explicit standalone button work outside normal view design. These apply/delete tools perform validation, normalization, writes, and readback verification where applicable. AppForm publishes only after a draft change or when the published form lags behind; views noop requests skip publish, app_custom_buttons_apply and app_associated_resources_apply publish after at least one write succeeds and expose no draft-only parameter, charts are immediate-live without publish and resolve targets by chart_id first then exact unique chart name, portal updates use replace semantics only when sections are supplied and edit-mode base-info-only updates may omit sections, portal delete separates DELETE execution from readback verification, portal pc layout is a 24-column grid and mobile is a 6-column grid so omit position or use layout_preset when unsure, publish=false only guarantees draft/base-info updates for tools that still expose that parameter, and flow should use app_flow_validate as the precheck before writing (it validates the drafted WorkflowSpec offline and never writes), while publish=false only guarantees draft/base-info updates and is a real draft write, not a precheck. "
|
|
43
44
|
"Builder apply/write outputs include schema_version, operation, summary, and resources[]; use resources[].id/key/name/ids/parent as the stable UI and agent display entry, and keep legacy fields such as field_diff/views_diff/chart_results only for compatibility or troubleshooting. "
|
|
45
|
+
"创建、修改或安装轻流资源后,最终回复主动提供主要结果的可点击页面入口,无需等待用户再次索要。"
|
|
46
|
+
"采用“完成结果 + 查看入口”,文案使用“在轻流中查看应用/应用包/视图”,不默认称为“继续编辑”。"
|
|
47
|
+
"默认只交付用户请求的主要结果:安装方案默认提供应用包入口;直接创建或修改应用、视图时提供对应入口;用户明确要求多个主要对象时分别提供。"
|
|
48
|
+
"使用业务结果中的稳定 `uri`,按 URI 去重,不为全部子资源预签票据;主要对象没有可靠 URI 时说明限制,不猜地址,也不替换为不相关页面。"
|
|
49
|
+
"部分成功时仅交付已确认成功且可定位的主要结果,保留失败或未验证说明。"
|
|
50
|
+
"纯数据问答、中间读取、失败资源和删除操作不自动签发;用户明确要求不提供链接时遵从。用户明确要求查看或打开页面时也使用已确认的路径。"
|
|
51
|
+
"目标浏览器登录态未知时,临近交付用资源 URI 的同源 pathname 调用 get_login_link,为每个实际展示的入口分别签发,使用返回的完整 URL,不额外追问登录态。"
|
|
52
|
+
"只有已有用户确认同一浏览器、网页域名、账号和工作区已登录且无失效信号的事实时,才直接交付稳定 URI;调用过票据工具、自动化浏览器已登录或之前签发过票据均不能证明用户浏览器已登录,已消费链接不可复用。"
|
|
53
|
+
"用户未要求打开时不提前打开或消费链接;浏览器不可用时提供可点击链接,不假称已经打开。"
|
|
54
|
+
"签发失败时说明免登录链接未生成,提供标注“需登录”的稳定 URI 作为备用入口,不重做业务操作。机器结果继续保留稳定 URI,临时登录链接仅用于面向用户交付。 "
|
|
44
55
|
"For existing object parameter replacement, prefer patch_views, patch_buttons, patch_resources, and patch_charts with set/unset; the tool reads current config and full-saves internally, while upsert_* is for creation or full target configuration and should not be used as an incomplete partial update. "
|
|
45
56
|
"For builder delete/remove apply results, separate delete execution from readback verification: after DELETE is sent, resources expose delete_executed, readback_status, and safe_to_retry_delete=false. If readback_status is unavailable or still_exists, do not blindly repeat the delete; confirm later with app_get/view_get/chart_get or the relevant apply readback. Views/buttons use single-item readback; associated resources use one app-level resource-pool readback because there is no confirmed single-item GET. "
|
|
46
57
|
"For builder write/apply results, separate write execution from final readback verification: status=partial_success with write_executed=true and safe_to_retry=false means the write was sent or succeeded, while final readback, publish verification, or metadata confirmation is pending or permission-restricted. Report it as written but unverified; do not repeat the same write blindly. "
|
|
@@ -52,7 +63,9 @@ def build_builder_server() -> FastMCP:
|
|
|
52
63
|
"For associated reports/views, use app_associated_resources_apply for both the app-level associated_resources pool and per-view display config; associated_item_id is the app-level form_asos_chart.id, and view_configs/remove/reorder may also pass an existing resource's chart_id/chart_key/view_key because the tool resolves those to the internal id. Before creating an associated resource, read app_get.associated_resources and reuse an existing matching target_app_key + view_key/chart_key through patch_resources; client_key only works inside one apply call and is not persisted. Do not ask agents to pass backend raw sourceType: views infer the internal Qingflow view source, reports default to BI app reports, and dataset reports use report_source=dataset. "
|
|
53
64
|
"For code_block fields with output bindings, always use qf_output assignment rather than const/let qf_output, and use app_repair_code_blocks when an existing form hangs because output-bound fields stay loading. "
|
|
54
65
|
"Use package_apply to manage package metadata, visibility, grouping, and ordering, and app_publish_verify for explicit final publish verification. "
|
|
55
|
-
"For workflow edits, use app_flow_get_schema/app_flow_get when raw WorkflowSpec shape is needed, then app_flow_apply with spec for full replacement or patch_nodes for targeted node edits. Declare node assignees and editable fields explicitly. "
|
|
66
|
+
"For workflow edits, use app_flow_get_schema/app_flow_get when raw WorkflowSpec shape is needed, then app_flow_validate on the drafted spec as a write-free precheck, then app_flow_apply with spec for full replacement or patch_nodes for targeted node edits. Declare node assignees and editable fields explicitly. Workflow node ids must round-trip: nodes[].id is the numeric audit_node_id string returned by app_flow_get, pass ids back unchanged and never invent them; a spec that drops existing nodes triggers a destructive two-phase apply — the first call returns status=confirmation_required with a preview blueprint, unrebuildable attachment warnings (eSign/print/chart/email/SMS/webhook), and a one-time confirm_ticket; re-invoke app_flow_apply with the same spec plus confirm_ticket to execute, or re-fetch the spec and reuse the original node ids if the intent was update. reason_code SPEC_CHANGED means the spec differed from the preview, BASELINE_DRIFTED means the draft changed underneath (re-fetch first). " "If builder writes are blocked by the current user's own edit lock, use app_release_edit_lock_if_mine with the lock owner details from the failed result. "
|
|
67
|
+
|
|
68
|
+
"For workflow edits, use app_flow_get_schema/app_flow_get when raw WorkflowSpec shape is needed, then app_flow_validate on the drafted spec as a write-free precheck, then app_flow_apply with spec for full replacement or patch_nodes for targeted node edits. Version recovery goes through app_flow_list_versions to discover snapshots (version_id/snapshot_type/time), optional app_flow_get(version_id=...) to inspect a target, then app_flow_restore_version to roll the draft spec back; restore is a draft-domain write under the same maintenance gate and reports readback verification against the target snapshot. Declare node assignees and editable fields explicitly. "
|
|
56
69
|
"If builder writes are blocked by the current user's own edit lock, use app_release_edit_lock_if_mine with the lock owner details from the failed result. "
|
|
57
70
|
"Do not handcraft internal solution payloads or rely on build_id/stage/repair. "
|
|
58
71
|
"If the current MCP capability is unsupported, the workflow is awkward, or the user's need still cannot be satisfied after reasonable use, first summarize the gap, ask whether to submit feedback, and call feedback_submit only after explicit user confirmation."
|
|
@@ -64,6 +77,7 @@ def build_builder_server() -> FastMCP:
|
|
|
64
77
|
workspace = wrap_trimmed_methods(WorkspaceTools(sessions, backend), BUILDER_SERVER_METHOD_MAP)
|
|
65
78
|
files = wrap_trimmed_methods(FileTools(sessions, backend), BUILDER_SERVER_METHOD_MAP)
|
|
66
79
|
ai_builder = wrap_trimmed_methods(AiBuilderTools(sessions, backend), BUILDER_SERVER_METHOD_MAP)
|
|
80
|
+
login_link = LoginLinkTools(sessions, backend)
|
|
67
81
|
feedback = FeedbackTools(backend, mcp_side="App Builder MCP")
|
|
68
82
|
|
|
69
83
|
@server.tool()
|
|
@@ -90,6 +104,10 @@ def build_builder_server() -> FastMCP:
|
|
|
90
104
|
def auth_logout(profile: str = DEFAULT_PROFILE, forget_persisted: bool = False) -> dict:
|
|
91
105
|
return auth.auth_logout(profile=profile, forget_persisted=forget_persisted)
|
|
92
106
|
|
|
107
|
+
@server.tool(description=LOGIN_LINK_DESCRIPTION)
|
|
108
|
+
def get_login_link(profile: str = DEFAULT_PROFILE, redirect_path: str = "") -> dict:
|
|
109
|
+
return login_link.get_login_link(profile=profile, redirect_path=redirect_path)
|
|
110
|
+
|
|
93
111
|
@server.tool()
|
|
94
112
|
def workspace_list(
|
|
95
113
|
profile: str = DEFAULT_PROFILE,
|
|
@@ -466,6 +484,41 @@ def build_builder_server() -> FastMCP:
|
|
|
466
484
|
def app_flow_get_schema(profile: str = DEFAULT_PROFILE, schema_version: str = "") -> dict:
|
|
467
485
|
return ai_builder.app_flow_get_schema(profile=profile, schema_version=schema_version or None)
|
|
468
486
|
|
|
487
|
+
@server.tool()
|
|
488
|
+
def app_flow_validate(
|
|
489
|
+
profile: str = DEFAULT_PROFILE,
|
|
490
|
+
spec: dict | None = None,
|
|
491
|
+
schema_version: str = "",
|
|
492
|
+
) -> dict:
|
|
493
|
+
return ai_builder.app_flow_validate(
|
|
494
|
+
profile=profile,
|
|
495
|
+
spec=spec or {},
|
|
496
|
+
schema_version=schema_version or None,
|
|
497
|
+
)
|
|
498
|
+
|
|
499
|
+
@server.tool()
|
|
500
|
+
def app_flow_list_versions(
|
|
501
|
+
profile: str = DEFAULT_PROFILE,
|
|
502
|
+
app_key: str = "",
|
|
503
|
+
) -> dict:
|
|
504
|
+
return ai_builder.app_flow_list_versions(profile=profile, app_key=app_key)
|
|
505
|
+
|
|
506
|
+
@server.tool()
|
|
507
|
+
def app_flow_restore_version(
|
|
508
|
+
profile: str = DEFAULT_PROFILE,
|
|
509
|
+
app_key: str = "",
|
|
510
|
+
version_id: str = "",
|
|
511
|
+
idempotency_key: str = "",
|
|
512
|
+
schema_version: str = "",
|
|
513
|
+
) -> dict:
|
|
514
|
+
return ai_builder.app_flow_restore_version(
|
|
515
|
+
profile=profile,
|
|
516
|
+
app_key=app_key,
|
|
517
|
+
version_id=version_id,
|
|
518
|
+
idempotency_key=idempotency_key or None,
|
|
519
|
+
schema_version=schema_version or None,
|
|
520
|
+
)
|
|
521
|
+
|
|
469
522
|
@server.tool()
|
|
470
523
|
def app_flow_apply(
|
|
471
524
|
profile: str = DEFAULT_PROFILE,
|
|
@@ -475,6 +528,7 @@ def build_builder_server() -> FastMCP:
|
|
|
475
528
|
idempotency_key: str = "",
|
|
476
529
|
schema_version: str = "",
|
|
477
530
|
patch_nodes: list[dict] | None = None,
|
|
531
|
+
confirm_ticket: str = "",
|
|
478
532
|
) -> dict:
|
|
479
533
|
return ai_builder.app_flow_apply(
|
|
480
534
|
profile=profile,
|
|
@@ -484,6 +538,7 @@ def build_builder_server() -> FastMCP:
|
|
|
484
538
|
idempotency_key=idempotency_key or None,
|
|
485
539
|
schema_version=schema_version or None,
|
|
486
540
|
patch_nodes=patch_nodes,
|
|
541
|
+
confirm_ticket=confirm_ticket or None,
|
|
487
542
|
)
|
|
488
543
|
|
|
489
544
|
@server.tool()
|
|
@@ -10,6 +10,7 @@ from .config import DEFAULT_PROFILE
|
|
|
10
10
|
from .response_trim import USER_SERVER_METHOD_MAP, trim_error_response, trim_public_response, wrap_trimmed_methods
|
|
11
11
|
from .session_store import SessionStore
|
|
12
12
|
from .tools.app_tools import AppTools
|
|
13
|
+
from .tools.data_tools import DataTools
|
|
13
14
|
from .tools.auth_tools import AuthTools
|
|
14
15
|
from .tools.code_block_tools import CodeBlockTools
|
|
15
16
|
from .tools.directory_tools import DirectoryTools
|
|
@@ -33,12 +34,15 @@ def build_user_server(*, stateless_http: bool = False) -> FastMCP:
|
|
|
33
34
|
|
|
34
35
|
## 页面交接
|
|
35
36
|
|
|
36
|
-
|
|
37
|
+
创建、修改或安装轻流资源后,最终回复主动提供主要结果的可点击页面入口,无需等待用户再次索要。采用“完成结果 + 查看入口”,文案使用“在轻流中查看应用/应用包/视图”,不默认称为“继续编辑”。用户明确要求查看或打开页面时也适用以下规则。
|
|
38
|
+
默认只交付用户请求的主要结果:安装方案默认提供应用包入口;直接创建或修改应用、视图时提供对应入口;用户明确要求多个主要对象时分别提供。使用业务结果中的稳定 `uri`,按 URI 去重,不为全部子资源预签票据。
|
|
39
|
+
主要对象没有可靠 URI 时说明限制,不猜地址,也不替换为不相关页面。部分成功时仅交付已确认成功且可定位的主要结果,保留失败或未验证说明。纯数据问答、中间读取、失败资源和删除操作不自动签发;用户明确要求不提供链接时遵从。
|
|
37
40
|
目标路径由业务工具或领域 Skill 根据真实前端路由确定,不由票据工具根据业务标识推导。
|
|
38
41
|
已知目标传可选 `redirect_path`;明确指定页面但路径未知时继续定位或说明限制,不猜测,也不静默改为首页。
|
|
39
42
|
搜索未命中时继续搜索 `login` 或查看服务工具列表,不擅自打开固定正式站点;鉴权或工具失败时如实说明。
|
|
40
|
-
|
|
41
|
-
|
|
43
|
+
目标浏览器登录态未知时,默认用资源 URI 的同源 pathname 调用 `get_login_link`,为每个实际展示的入口分别签发,不额外追问登录态;只有已有用户确认同一浏览器、网页域名、账号和工作区已登录且无失效信号的事实时,才直接交付稳定 URI。调用过票据工具、自动化浏览器已登录或之前签发过票据均不能证明用户浏览器已登录,已消费链接也不能复用。
|
|
44
|
+
临近交付时再签发,使用返回的完整 URL。用户未要求打开时仅交付,不提前消费;要求“打开”时才调用浏览器。签发失败时说明免登录链接未生成,并提供标注“需登录”的稳定 URI 作为备用入口,不重做业务操作。机器结果继续保留稳定 URI,临时登录链接仅用于面向用户交付。
|
|
45
|
+
浏览器不可用时提供可点击链接,不假称已经打开,不展示工作区数字 ID 或原始 JSON。
|
|
42
46
|
链接约 5 分钟内一次性有效,不能作为长期书签;安全验证及原有业务权限不变。
|
|
43
47
|
|
|
44
48
|
## App Discovery
|
|
@@ -239,6 +243,7 @@ If the current MCP capability is unsupported, the workflow is awkward, or the us
|
|
|
239
243
|
backend = BackendClient()
|
|
240
244
|
auth = wrap_trimmed_methods(AuthTools(sessions, backend), USER_SERVER_METHOD_MAP)
|
|
241
245
|
apps = wrap_trimmed_methods(AppTools(sessions, backend), USER_SERVER_METHOD_MAP)
|
|
246
|
+
data_tools = wrap_trimmed_methods(DataTools(sessions, backend), USER_SERVER_METHOD_MAP)
|
|
242
247
|
workspace = wrap_trimmed_methods(WorkspaceTools(sessions, backend), USER_SERVER_METHOD_MAP)
|
|
243
248
|
file_tools = wrap_trimmed_methods(FileTools(sessions, backend), USER_SERVER_METHOD_MAP)
|
|
244
249
|
imports = wrap_trimmed_methods(ImportTools(sessions, backend), USER_SERVER_METHOD_MAP)
|
|
@@ -392,6 +397,8 @@ If the current MCP capability is unsupported, the workflow is awkward, or the us
|
|
|
392
397
|
def app_get(profile: str = DEFAULT_PROFILE, app_key: str = "") -> dict:
|
|
393
398
|
return apps.app_get(profile=profile, app_key=app_key)
|
|
394
399
|
|
|
400
|
+
data_tools.register(server)
|
|
401
|
+
|
|
395
402
|
@server.tool()
|
|
396
403
|
def portal_list(profile: str = DEFAULT_PROFILE) -> dict:
|
|
397
404
|
return resources.portal_list(profile=profile)
|