@qingflow-tech/qingflow-app-builder-mcp 1.0.4 → 1.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/package.json +1 -1
- package/pyproject.toml +2 -1
- package/src/qingflow_mcp/backend_client.py +55 -1
- package/src/qingflow_mcp/cli/commands/record.py +63 -6
- package/src/qingflow_mcp/cli/formatters.py +101 -1
- package/src/qingflow_mcp/public_surface.py +2 -1
- package/src/qingflow_mcp/response_trim.py +235 -10
- package/src/qingflow_mcp/server.py +19 -12
- package/src/qingflow_mcp/server_app_user.py +30 -13
- package/src/qingflow_mcp/tools/record_tools.py +13425 -8817
package/README.md
CHANGED
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
Install:
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
|
-
npm install @qingflow-tech/qingflow-app-builder-mcp@1.0.
|
|
6
|
+
npm install @qingflow-tech/qingflow-app-builder-mcp@1.0.6
|
|
7
7
|
```
|
|
8
8
|
|
|
9
9
|
Run:
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
|
-
npx -y -p @qingflow-tech/qingflow-app-builder-mcp@1.0.
|
|
12
|
+
npx -y -p @qingflow-tech/qingflow-app-builder-mcp@1.0.6 qingflow-app-builder-mcp
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
Environment:
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "qingflow-mcp"
|
|
7
|
-
version = "1.0.
|
|
7
|
+
version = "1.0.6"
|
|
8
8
|
description = "User-authenticated MCP server for Qingflow"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -29,6 +29,7 @@ dependencies = [
|
|
|
29
29
|
"openpyxl>=3.1,<4.0",
|
|
30
30
|
"pydantic>=2.8,<3.0",
|
|
31
31
|
"pycryptodome>=3.20,<4.0",
|
|
32
|
+
"pypdf>=5.0,<6.0",
|
|
32
33
|
"python-socketio[client]>=5.11,<6.0",
|
|
33
34
|
]
|
|
34
35
|
|
|
@@ -4,7 +4,7 @@ from dataclasses import dataclass
|
|
|
4
4
|
from threading import Event
|
|
5
5
|
from time import sleep
|
|
6
6
|
from typing import Any
|
|
7
|
-
from urllib.parse import urlsplit, urlunsplit
|
|
7
|
+
from urllib.parse import urljoin, urlsplit, urlunsplit
|
|
8
8
|
from uuid import uuid4
|
|
9
9
|
|
|
10
10
|
import httpx
|
|
@@ -33,6 +33,15 @@ class BackendResponse:
|
|
|
33
33
|
qf_response_version: str | None = None
|
|
34
34
|
|
|
35
35
|
|
|
36
|
+
@dataclass(slots=True)
|
|
37
|
+
class BackendBinaryResponse:
|
|
38
|
+
content: bytes
|
|
39
|
+
headers: dict[str, str]
|
|
40
|
+
http_status: int
|
|
41
|
+
final_url: str
|
|
42
|
+
redirected: bool
|
|
43
|
+
|
|
44
|
+
|
|
36
45
|
class BackendClient:
|
|
37
46
|
def __init__(self, timeout: float | None = None, client: httpx.Client | None = None) -> None:
|
|
38
47
|
self._owns_client = client is None
|
|
@@ -290,6 +299,51 @@ class BackendClient:
|
|
|
290
299
|
)
|
|
291
300
|
return response.content
|
|
292
301
|
|
|
302
|
+
def download_binary_with_cookie(
|
|
303
|
+
self,
|
|
304
|
+
context: BackendRequestContext,
|
|
305
|
+
url: str,
|
|
306
|
+
*,
|
|
307
|
+
cookie_name: str,
|
|
308
|
+
headers: dict[str, str] | None = None,
|
|
309
|
+
) -> BackendBinaryResponse:
|
|
310
|
+
qf_version, _source = self._resolve_qf_version(context.qf_version)
|
|
311
|
+
request_headers = {
|
|
312
|
+
"User-Agent": DEFAULT_USER_AGENT,
|
|
313
|
+
"Qf-Request-Id": context.qf_request_id or str(uuid4()),
|
|
314
|
+
}
|
|
315
|
+
if headers:
|
|
316
|
+
request_headers.update({key: value for key, value in headers.items() if value is not None})
|
|
317
|
+
cookie_parts = [f"{cookie_name}={context.token}"]
|
|
318
|
+
if qf_version:
|
|
319
|
+
cookie_parts.append(f"qfVersion={qf_version}")
|
|
320
|
+
request_headers["Cookie"] = "; ".join(cookie_parts)
|
|
321
|
+
try:
|
|
322
|
+
response = self._client.get(url, headers=request_headers, follow_redirects=False)
|
|
323
|
+
except httpx.RequestError as exc:
|
|
324
|
+
raise QingflowApiError(category="network", message=str(exc))
|
|
325
|
+
redirected = 300 <= response.status_code < 400 and bool(response.headers.get("location"))
|
|
326
|
+
if redirected:
|
|
327
|
+
redirect_headers = {key: value for key, value in request_headers.items() if key.lower() != "cookie"}
|
|
328
|
+
redirect_url = urljoin(str(response.url), response.headers["location"])
|
|
329
|
+
try:
|
|
330
|
+
response = self._client.get(redirect_url, headers=redirect_headers)
|
|
331
|
+
except httpx.RequestError as exc:
|
|
332
|
+
raise QingflowApiError(category="network", message=str(exc))
|
|
333
|
+
if response.status_code >= 400:
|
|
334
|
+
raise QingflowApiError(
|
|
335
|
+
category="http",
|
|
336
|
+
message=self._extract_message(response.text) or f"HTTP {response.status_code}",
|
|
337
|
+
http_status=response.status_code,
|
|
338
|
+
)
|
|
339
|
+
return BackendBinaryResponse(
|
|
340
|
+
content=response.content,
|
|
341
|
+
headers=dict(response.headers),
|
|
342
|
+
http_status=response.status_code,
|
|
343
|
+
final_url=str(response.url),
|
|
344
|
+
redirected=redirected or bool(response.history) or str(response.url) != url,
|
|
345
|
+
)
|
|
346
|
+
|
|
293
347
|
def request_multipart(
|
|
294
348
|
self,
|
|
295
349
|
method: str,
|
|
@@ -10,7 +10,11 @@ from .common import load_list_arg, load_object_arg, raise_config_error, require_
|
|
|
10
10
|
|
|
11
11
|
def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
|
|
12
12
|
parser = subparsers.add_parser("record", help="记录与表结构")
|
|
13
|
-
record_subparsers = parser.add_subparsers(
|
|
13
|
+
record_subparsers = parser.add_subparsers(
|
|
14
|
+
dest="record_command",
|
|
15
|
+
required=True,
|
|
16
|
+
metavar="{schema,list,access,get,insert,update,delete,code-block-run}",
|
|
17
|
+
)
|
|
14
18
|
|
|
15
19
|
schema = record_subparsers.add_parser("schema", help="读取记录相关表结构")
|
|
16
20
|
schema.add_argument("--mode", dest="legacy_mode", help=argparse.SUPPRESS)
|
|
@@ -47,9 +51,11 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
|
|
|
47
51
|
list_parser.add_argument("--app-key", required=True)
|
|
48
52
|
list_parser.add_argument("--column", dest="columns", action="append", type=int, default=[])
|
|
49
53
|
list_parser.add_argument("--columns-file")
|
|
54
|
+
list_parser.add_argument("--query")
|
|
55
|
+
list_parser.add_argument("--query-field", dest="query_fields", action="append", type=int, default=[])
|
|
56
|
+
list_parser.add_argument("--query-fields-file")
|
|
50
57
|
list_parser.add_argument("--where-file")
|
|
51
58
|
list_parser.add_argument("--order-by-file")
|
|
52
|
-
list_parser.add_argument("--limit", type=int, default=20)
|
|
53
59
|
list_parser.add_argument("--page", type=int, default=1)
|
|
54
60
|
list_parser.add_argument("--view-id")
|
|
55
61
|
list_parser.add_argument("--list-type", dest="legacy_list_type", type=int, help=argparse.SUPPRESS)
|
|
@@ -57,17 +63,27 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
|
|
|
57
63
|
list_parser.add_argument("--view-name", dest="legacy_view_name", help=argparse.SUPPRESS)
|
|
58
64
|
list_parser.set_defaults(handler=_handle_list, format_hint="record_list")
|
|
59
65
|
|
|
66
|
+
access_parser = record_subparsers.add_parser("access", help="访问记录并写入本地 CSV 分片")
|
|
67
|
+
access_parser.add_argument("--app-key", required=True)
|
|
68
|
+
access_parser.add_argument("--column", dest="columns", action="append", type=int, default=[])
|
|
69
|
+
access_parser.add_argument("--columns-file")
|
|
70
|
+
access_parser.add_argument("--where-file")
|
|
71
|
+
access_parser.add_argument("--order-by-file")
|
|
72
|
+
access_parser.add_argument("--view-id", required=True)
|
|
73
|
+
access_parser.set_defaults(handler=_handle_access, format_hint="record_access")
|
|
74
|
+
|
|
60
75
|
get = record_subparsers.add_parser("get", help="读取单条记录")
|
|
61
76
|
get.add_argument("--app-key", required=True)
|
|
62
77
|
get.add_argument("--record-id", required=True)
|
|
63
78
|
get.add_argument("--column", dest="columns", action="append", type=int, default=[])
|
|
64
79
|
get.add_argument("--columns-file")
|
|
65
80
|
get.add_argument("--view-id")
|
|
66
|
-
get.set_defaults(handler=_handle_get, format_hint="")
|
|
81
|
+
get.set_defaults(handler=_handle_get, format_hint="record_get")
|
|
67
82
|
|
|
68
83
|
insert = record_subparsers.add_parser("insert", help="新增记录")
|
|
69
84
|
insert.add_argument("--app-key", required=True)
|
|
70
|
-
insert.add_argument("--fields-file"
|
|
85
|
+
insert.add_argument("--fields-file")
|
|
86
|
+
insert.add_argument("--items-file")
|
|
71
87
|
insert.add_argument("--verify-write", action=argparse.BooleanOptionalAction, default=True)
|
|
72
88
|
insert.set_defaults(handler=_handle_insert, format_hint="")
|
|
73
89
|
|
|
@@ -86,7 +102,12 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
|
|
|
86
102
|
delete.add_argument("--record-ids-file")
|
|
87
103
|
delete.set_defaults(handler=_handle_delete, format_hint="")
|
|
88
104
|
|
|
89
|
-
analyze = record_subparsers.add_parser("analyze", help=
|
|
105
|
+
analyze = record_subparsers.add_parser("analyze", help=argparse.SUPPRESS)
|
|
106
|
+
record_subparsers._choices_actions = [ # type: ignore[attr-defined]
|
|
107
|
+
action
|
|
108
|
+
for action in record_subparsers._choices_actions # type: ignore[attr-defined]
|
|
109
|
+
if getattr(action, "dest", None) != "analyze"
|
|
110
|
+
]
|
|
90
111
|
analyze.add_argument("--app-key", required=True)
|
|
91
112
|
analyze.add_argument("--dimensions-file")
|
|
92
113
|
analyze.add_argument("--metrics-file")
|
|
@@ -122,6 +143,13 @@ def _columns(args: argparse.Namespace) -> list[Any]:
|
|
|
122
143
|
return columns
|
|
123
144
|
|
|
124
145
|
|
|
146
|
+
def _query_fields(args: argparse.Namespace) -> list[Any]:
|
|
147
|
+
query_fields: list[Any] = list(getattr(args, "query_fields", None) or [])
|
|
148
|
+
if getattr(args, "query_fields_file", None):
|
|
149
|
+
query_fields.extend(require_list_arg(args.query_fields_file, option_name="--query-fields-file"))
|
|
150
|
+
return query_fields
|
|
151
|
+
|
|
152
|
+
|
|
125
153
|
def _handle_schema_root(args: argparse.Namespace, _context: CliContext) -> dict:
|
|
126
154
|
mode = (args.legacy_mode or "").strip()
|
|
127
155
|
if mode:
|
|
@@ -216,14 +244,26 @@ def _handle_list(args: argparse.Namespace, context: CliContext) -> dict:
|
|
|
216
244
|
profile=args.profile,
|
|
217
245
|
app_key=args.app_key,
|
|
218
246
|
columns=_columns(args),
|
|
247
|
+
query=args.query,
|
|
248
|
+
query_fields=_query_fields(args),
|
|
219
249
|
where=load_list_arg(args.where_file, option_name="--where-file"),
|
|
220
250
|
order_by=load_list_arg(args.order_by_file, option_name="--order-by-file"),
|
|
221
|
-
limit=args.limit,
|
|
222
251
|
page=args.page,
|
|
223
252
|
view_id=args.view_id,
|
|
224
253
|
)
|
|
225
254
|
|
|
226
255
|
|
|
256
|
+
def _handle_access(args: argparse.Namespace, context: CliContext) -> dict:
|
|
257
|
+
return context.record.record_access(
|
|
258
|
+
profile=args.profile,
|
|
259
|
+
app_key=args.app_key,
|
|
260
|
+
columns=_columns(args),
|
|
261
|
+
where=load_list_arg(args.where_file, option_name="--where-file"),
|
|
262
|
+
order_by=load_list_arg(args.order_by_file, option_name="--order-by-file"),
|
|
263
|
+
view_id=args.view_id,
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
|
|
227
267
|
def _handle_get(args: argparse.Namespace, context: CliContext) -> dict:
|
|
228
268
|
return context.record.record_get_public(
|
|
229
269
|
profile=args.profile,
|
|
@@ -235,6 +275,23 @@ def _handle_get(args: argparse.Namespace, context: CliContext) -> dict:
|
|
|
235
275
|
|
|
236
276
|
|
|
237
277
|
def _handle_insert(args: argparse.Namespace, context: CliContext) -> dict:
|
|
278
|
+
if args.items_file:
|
|
279
|
+
if args.fields_file:
|
|
280
|
+
raise_config_error(
|
|
281
|
+
"record insert batch mode does not accept --fields-file.",
|
|
282
|
+
fix_hint="Use `record insert --app-key APP_KEY --items-file ITEMS.json` for batch inserts.",
|
|
283
|
+
)
|
|
284
|
+
return context.record.record_insert_public(
|
|
285
|
+
profile=args.profile,
|
|
286
|
+
app_key=args.app_key,
|
|
287
|
+
items=require_list_arg(args.items_file, option_name="--items-file"),
|
|
288
|
+
verify_write=bool(args.verify_write),
|
|
289
|
+
)
|
|
290
|
+
if not args.fields_file:
|
|
291
|
+
raise_config_error(
|
|
292
|
+
"record insert requires --items-file or --fields-file.",
|
|
293
|
+
fix_hint="Prefer `record insert --app-key APP_KEY --items-file ITEMS.json`; use --fields-file only for legacy single inserts.",
|
|
294
|
+
)
|
|
238
295
|
return context.record.record_insert_public(
|
|
239
296
|
profile=args.profile,
|
|
240
297
|
app_key=args.app_key,
|
|
@@ -183,7 +183,17 @@ def _format_app_get(result: dict[str, Any]) -> str:
|
|
|
183
183
|
def _format_record_list(result: dict[str, Any]) -> str:
|
|
184
184
|
data = result.get("data") if isinstance(result.get("data"), dict) else {}
|
|
185
185
|
items = data.get("items") if isinstance(data.get("items"), list) else []
|
|
186
|
-
lines = [
|
|
186
|
+
lines: list[str] = []
|
|
187
|
+
lookup = result.get("lookup") if isinstance(result.get("lookup"), dict) else {}
|
|
188
|
+
if lookup:
|
|
189
|
+
lines.append("Lookup:")
|
|
190
|
+
lines.append(f"- query: {lookup.get('query')}")
|
|
191
|
+
lines.append(f"- confidence: {lookup.get('confidence')}")
|
|
192
|
+
lines.append(f"- next_action: {lookup.get('next_action')}")
|
|
193
|
+
lines.append(f"- total_count: {lookup.get('total_count')}")
|
|
194
|
+
lines.append(f"- returned_count: {lookup.get('returned_count')}")
|
|
195
|
+
lines.append(f"- truncated: {lookup.get('truncated')}")
|
|
196
|
+
lines.append(f"Returned Records: {len(items)}")
|
|
187
197
|
for item in items[:10]:
|
|
188
198
|
if isinstance(item, dict):
|
|
189
199
|
lines.append(json.dumps(item, ensure_ascii=False))
|
|
@@ -194,6 +204,94 @@ def _format_record_list(result: dict[str, Any]) -> str:
|
|
|
194
204
|
return "\n".join(lines) + "\n"
|
|
195
205
|
|
|
196
206
|
|
|
207
|
+
def _format_record_access(result: dict[str, Any]) -> str:
|
|
208
|
+
status = result.get("status") or "-"
|
|
209
|
+
lines = [
|
|
210
|
+
f"Status: {status}",
|
|
211
|
+
f"Rows: {result.get('row_count')}",
|
|
212
|
+
f"Complete: {result.get('complete')}",
|
|
213
|
+
f"Safe for final conclusion: {result.get('safe_for_final_conclusion')}",
|
|
214
|
+
]
|
|
215
|
+
if result.get("local_dir"):
|
|
216
|
+
lines.append(f"Local dir: {result.get('local_dir')}")
|
|
217
|
+
files = result.get("files") if isinstance(result.get("files"), list) else []
|
|
218
|
+
if files:
|
|
219
|
+
lines.append("Files:")
|
|
220
|
+
for item in files:
|
|
221
|
+
if isinstance(item, dict):
|
|
222
|
+
lines.append(f"- part {item.get('part')}: {item.get('local_path')} ({item.get('row_count')} rows)")
|
|
223
|
+
scope = result.get("scope") if isinstance(result.get("scope"), dict) else {}
|
|
224
|
+
if status == "needs_scope" and scope:
|
|
225
|
+
lines.append("Scope required:")
|
|
226
|
+
lines.append(f"- reported_total: {scope.get('reported_total')}")
|
|
227
|
+
lines.append(f"- estimated_pages: {scope.get('estimated_pages')}")
|
|
228
|
+
suggested = scope.get("suggested_time_fields") if isinstance(scope.get("suggested_time_fields"), list) else []
|
|
229
|
+
if suggested:
|
|
230
|
+
names = ", ".join(str(item.get("title") or item.get("field_id")) for item in suggested if isinstance(item, dict))
|
|
231
|
+
lines.append(f"- suggested_time_fields: {names}")
|
|
232
|
+
_append_warnings(lines, result.get("warnings"))
|
|
233
|
+
_append_verification(lines, result.get("verification"))
|
|
234
|
+
return "\n".join(lines) + "\n"
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _format_record_get(result: dict[str, Any]) -> str:
|
|
238
|
+
record = result.get("record") if isinstance(result.get("record"), dict) else {}
|
|
239
|
+
app = result.get("app") if isinstance(result.get("app"), dict) else {}
|
|
240
|
+
view = result.get("view") if isinstance(result.get("view"), dict) else {}
|
|
241
|
+
fields = result.get("fields") if isinstance(result.get("fields"), list) else []
|
|
242
|
+
data_logs = result.get("data_logs") if isinstance(result.get("data_logs"), dict) else {}
|
|
243
|
+
workflow_logs = result.get("workflow_logs") if isinstance(result.get("workflow_logs"), dict) else {}
|
|
244
|
+
media_assets = result.get("media_assets") if isinstance(result.get("media_assets"), dict) else {}
|
|
245
|
+
media_items = media_assets.get("items") if isinstance(media_assets.get("items"), list) else []
|
|
246
|
+
downloaded_media = [item for item in media_items if isinstance(item, dict) and item.get("access_status") == "downloaded"]
|
|
247
|
+
failed_media = [item for item in media_items if isinstance(item, dict) and item.get("access_status") != "downloaded"]
|
|
248
|
+
file_assets = result.get("file_assets") if isinstance(result.get("file_assets"), dict) else {}
|
|
249
|
+
file_items = file_assets.get("items") if isinstance(file_assets.get("items"), list) else []
|
|
250
|
+
downloaded_files = [item for item in file_items if isinstance(item, dict) and item.get("access_status") == "downloaded"]
|
|
251
|
+
failed_files = [item for item in file_items if isinstance(item, dict) and item.get("access_status") != "downloaded"]
|
|
252
|
+
extracted_files = [
|
|
253
|
+
item
|
|
254
|
+
for item in downloaded_files
|
|
255
|
+
if isinstance(item.get("extraction"), dict) and item["extraction"].get("status") == "ok"
|
|
256
|
+
]
|
|
257
|
+
associated_resources = result.get("associated_resources") if isinstance(result.get("associated_resources"), list) else []
|
|
258
|
+
unavailable_context = result.get("unavailable_context") if isinstance(result.get("unavailable_context"), list) else []
|
|
259
|
+
lines = [
|
|
260
|
+
f"Status: {result.get('status') or '-'}",
|
|
261
|
+
f"App: {app.get('app_name') or app.get('app_key') or '-'} ({app.get('app_key') or '-'})",
|
|
262
|
+
f"View: {view.get('name') or view.get('view_id') or '-'}",
|
|
263
|
+
f"Record: {record.get('title') or '-'} ({record.get('record_id') or '-'})",
|
|
264
|
+
f"Fields: {len(fields)}",
|
|
265
|
+
f"Data logs: {data_logs.get('status') or '-'} / loaded={data_logs.get('items_loaded')}",
|
|
266
|
+
f"Workflow logs: {workflow_logs.get('status') or '-'} / loaded={workflow_logs.get('items_loaded')}",
|
|
267
|
+
f"Media assets: {media_assets.get('status') or '-'} / downloaded={len(downloaded_media)} / failed={len(failed_media)}",
|
|
268
|
+
f"File assets: {file_assets.get('status') or '-'} / downloaded={len(downloaded_files)} / extracted={len(extracted_files)} / failed={len(failed_files)}",
|
|
269
|
+
f"Associated resources: {len(associated_resources)}",
|
|
270
|
+
f"Unavailable contexts: {len(unavailable_context)}",
|
|
271
|
+
]
|
|
272
|
+
if media_assets.get("local_dir"):
|
|
273
|
+
lines.append(f"Media dir: {media_assets.get('local_dir')}")
|
|
274
|
+
if file_assets.get("local_dir"):
|
|
275
|
+
lines.append(f"File dir: {file_assets.get('local_dir')}")
|
|
276
|
+
if failed_media:
|
|
277
|
+
failure_counts: dict[str, int] = {}
|
|
278
|
+
for item in failed_media:
|
|
279
|
+
key = f"{item.get('access_status') or 'unknown'} via {item.get('download_strategy') or 'unknown'}"
|
|
280
|
+
failure_counts[key] = failure_counts.get(key, 0) + 1
|
|
281
|
+
lines.append("Media failures: " + ", ".join(f"{key}={count}" for key, count in sorted(failure_counts.items())))
|
|
282
|
+
if failed_files:
|
|
283
|
+
failure_counts = {}
|
|
284
|
+
for item in failed_files:
|
|
285
|
+
key = f"{item.get('access_status') or 'unknown'} via {item.get('download_strategy') or 'unknown'}"
|
|
286
|
+
failure_counts[key] = failure_counts.get(key, 0) + 1
|
|
287
|
+
lines.append("File failures: " + ", ".join(f"{key}={count}" for key, count in sorted(failure_counts.items())))
|
|
288
|
+
summary = result.get("summary") if isinstance(result.get("summary"), dict) else {}
|
|
289
|
+
if summary.get("text"):
|
|
290
|
+
lines.append(f"Summary: {summary.get('text')}")
|
|
291
|
+
_append_warnings(lines, result.get("warnings"))
|
|
292
|
+
return "\n".join(lines) + "\n"
|
|
293
|
+
|
|
294
|
+
|
|
197
295
|
def _format_task_list(result: dict[str, Any]) -> str:
|
|
198
296
|
data = result.get("data") if isinstance(result.get("data"), dict) else {}
|
|
199
297
|
items = data.get("items") if isinstance(data.get("items"), list) else []
|
|
@@ -693,6 +791,8 @@ _FORMATTERS = {
|
|
|
693
791
|
"app_search": _format_app_items,
|
|
694
792
|
"app_get": _format_app_get,
|
|
695
793
|
"record_list": _format_record_list,
|
|
794
|
+
"record_access": _format_record_access,
|
|
795
|
+
"record_get": _format_record_get,
|
|
696
796
|
"task_list": _format_task_list,
|
|
697
797
|
"task_workbench": _format_task_workbench,
|
|
698
798
|
"task_get": _format_task_get,
|
|
@@ -80,8 +80,9 @@ USER_PUBLIC_TOOL_SPECS: tuple[PublicToolSpec, ...] = (
|
|
|
80
80
|
),
|
|
81
81
|
PublicToolSpec(USER_DOMAIN, "record_member_candidates", ("record_member_candidates",), cli_public=False),
|
|
82
82
|
PublicToolSpec(USER_DOMAIN, "record_department_candidates", ("record_department_candidates",), cli_public=False),
|
|
83
|
-
PublicToolSpec(USER_DOMAIN, "record_analyze", ("record_analyze",), ("record", "analyze")),
|
|
83
|
+
PublicToolSpec(USER_DOMAIN, "record_analyze", ("record_analyze",), ("record", "analyze"), mcp_public=False),
|
|
84
84
|
PublicToolSpec(USER_DOMAIN, "record_list", ("record_list",), ("record", "list"), cli_show_effective_context=True),
|
|
85
|
+
PublicToolSpec(USER_DOMAIN, "record_access", ("record_access",), ("record", "access"), cli_show_effective_context=True),
|
|
85
86
|
PublicToolSpec(USER_DOMAIN, "record_get", ("record_get_public",), ("record", "get"), cli_show_effective_context=True),
|
|
86
87
|
PublicToolSpec(USER_DOMAIN, "record_insert", ("record_insert_public",), ("record", "insert"), cli_show_effective_context=True, cli_context_write=True),
|
|
87
88
|
PublicToolSpec(USER_DOMAIN, "record_update", ("record_update_public",), ("record", "update"), cli_show_effective_context=True, cli_context_write=True),
|