@josephyan/qingflow-app-builder-mcp 0.2.0-beta.1008 → 0.2.0-beta.1011

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 CHANGED
@@ -3,13 +3,13 @@
3
3
  Install:
4
4
 
5
5
  ```bash
6
- npm install @josephyan/qingflow-app-builder-mcp@0.2.0-beta.1008
6
+ npm install @josephyan/qingflow-app-builder-mcp@0.2.0-beta.1011
7
7
  ```
8
8
 
9
9
  Run:
10
10
 
11
11
  ```bash
12
- npx -y -p @josephyan/qingflow-app-builder-mcp@0.2.0-beta.1008 qingflow-app-builder-mcp
12
+ npx -y -p @josephyan/qingflow-app-builder-mcp@0.2.0-beta.1011 qingflow-app-builder-mcp
13
13
  ```
14
14
 
15
15
  Environment:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@josephyan/qingflow-app-builder-mcp",
3
- "version": "0.2.0-beta.1008",
3
+ "version": "0.2.0-beta.1011",
4
4
  "description": "Builder MCP for Qingflow app/package/system design and staged solution workflows.",
5
5
  "license": "MIT",
6
6
  "type": "module",
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 = "0.2.0b1008"
7
+ version = "0.2.0b1011"
8
8
  description = "User-authenticated MCP server for Qingflow"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -5,7 +5,7 @@ from pathlib import Path
5
5
 
6
6
  __all__ = ["__version__"]
7
7
 
8
- _FALLBACK_VERSION = "0.2.0b1008"
8
+ _FALLBACK_VERSION = "0.2.0b1011"
9
9
 
10
10
 
11
11
  def _resolve_local_pyproject_version() -> str | None:
@@ -474,6 +474,115 @@ class BackendClient:
474
474
  pass
475
475
  return import_result
476
476
 
477
+ def start_socket_record_export(
478
+ self,
479
+ context: BackendRequestContext,
480
+ *,
481
+ app_key: str,
482
+ view_id: str,
483
+ filter_bean: JSONObject,
484
+ export_config: JSONObject,
485
+ view_key: str | None = None,
486
+ result_amount: int = 0,
487
+ ack_timeout_seconds: float = 8.0,
488
+ ) -> dict[str, Any]:
489
+ try:
490
+ import socketio # type: ignore[import-not-found]
491
+ except ImportError as exc:
492
+ raise QingflowApiError(
493
+ category="config",
494
+ message=f"socket.io client dependency is missing: {exc}",
495
+ )
496
+
497
+ socket_base_url = self._build_socket_base_url(context.base_url)
498
+ export_result: dict[str, Any] = {
499
+ "backend_export_id": None,
500
+ "warnings": [],
501
+ }
502
+ sio = socketio.Client(reconnection=False, logger=False, engineio_logger=False)
503
+ event_name = "excelViewgraph" if view_key else "excel"
504
+ event_args: tuple[Any, ...]
505
+ if view_key:
506
+ event_args = (
507
+ context.token,
508
+ None,
509
+ view_key,
510
+ filter_bean,
511
+ export_config,
512
+ int(result_amount),
513
+ )
514
+ else:
515
+ event_args = (
516
+ context.token,
517
+ app_key,
518
+ filter_bean,
519
+ export_config,
520
+ int(result_amount),
521
+ )
522
+ try:
523
+ sio.connect(
524
+ socket_base_url,
525
+ transports=["websocket"],
526
+ socketio_path="socket.io",
527
+ headers=self._base_headers(
528
+ context.token,
529
+ context.ws_id,
530
+ qf_version=context.qf_version,
531
+ ),
532
+ wait_timeout=ack_timeout_seconds,
533
+ )
534
+ sio.emit("token", context.token)
535
+ sleep(0.2)
536
+ ack = sio.call(
537
+ event_name,
538
+ event_args,
539
+ timeout=ack_timeout_seconds,
540
+ )
541
+ ack_payload = ack[0] if isinstance(ack, list) and ack else ack
542
+ export_id: Any = ack_payload
543
+ if isinstance(ack_payload, dict):
544
+ error_code = ack_payload.get("error")
545
+ ack_message = ack_payload.get("message")
546
+ export_id = ack_payload.get("data")
547
+ if isinstance(export_id, dict):
548
+ export_id = (
549
+ export_id.get("exportId")
550
+ or export_id.get("export_id")
551
+ or export_id.get("id")
552
+ )
553
+ if error_code not in (None, 0):
554
+ raise QingflowApiError(
555
+ category="backend",
556
+ message=str(ack_message or f"socket export rejected with error {error_code}"),
557
+ details={
558
+ "socket_error_code": error_code,
559
+ "app_key": app_key,
560
+ "view_id": view_id,
561
+ "view_key": view_key,
562
+ },
563
+ )
564
+ if not export_id:
565
+ raise QingflowApiError(category="backend", message="socket export ack did not return export_id")
566
+ export_result["backend_export_id"] = str(export_id)
567
+ except Exception as exc:
568
+ message = str(exc)
569
+ if "timeout" in message.lower():
570
+ raise QingflowApiError(
571
+ category="network",
572
+ message="socket export ack timed out",
573
+ details={"error_code": "EXPORT_SOCKET_ACK_TIMEOUT"},
574
+ )
575
+ if isinstance(exc, QingflowApiError):
576
+ raise
577
+ raise QingflowApiError(category="network", message=message or "socket export failed")
578
+ finally:
579
+ try:
580
+ if sio.connected:
581
+ sio.disconnect()
582
+ except Exception:
583
+ pass
584
+ return export_result
585
+
477
586
  def _request_with_meta(
478
587
  self,
479
588
  method: str,
@@ -2,7 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import argparse
4
4
 
5
- from . import app, auth, builder, chart, imports, portal, record, task, view, workspace
5
+ from . import app, auth, builder, chart, exports, imports, portal, record, task, view, workspace
6
6
 
7
7
 
8
8
  def register_all_commands(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
@@ -14,5 +14,6 @@ def register_all_commands(subparsers: argparse._SubParsersAction[argparse.Argume
14
14
  chart.register(subparsers)
15
15
  record.register(subparsers)
16
16
  imports.register(subparsers)
17
+ exports.register(subparsers)
17
18
  task.register(subparsers)
18
19
  builder.register(subparsers)
@@ -0,0 +1,95 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+
5
+ from ..context import CliContext
6
+ from .common import load_list_arg
7
+
8
+
9
+ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
10
+ parser = subparsers.add_parser("export", help="导出")
11
+ export_subparsers = parser.add_subparsers(dest="export_command", required=True)
12
+
13
+ start = export_subparsers.add_parser("start", help="启动导出")
14
+ start.add_argument("--app-key", required=True)
15
+ start.add_argument("--view-id", default="system:all")
16
+ start.add_argument("--column", dest="columns", action="append", type=int, default=[], help="只导出这些 field_id;不传时导出当前视图全部字段")
17
+ start.add_argument("--columns-file", help="JSON/YAML list,内容与 --column 语义一致")
18
+ start.add_argument("--record-id", dest="record_ids", action="append", default=[], help="只导出这些 record_id;不传时导出当前视图全部数据")
19
+ start.add_argument("--record-ids-file", help="JSON/YAML list,内容与 --record-id 语义一致")
20
+ start.add_argument("--include-workflow-log", action=argparse.BooleanOptionalAction, default=False, help="是否同时导出流程日志")
21
+ start.set_defaults(handler=_handle_start, format_hint="export_start")
22
+
23
+ status = export_subparsers.add_parser("status", help="查询导出状态")
24
+ status.add_argument("--export-handle", required=True)
25
+ status.set_defaults(handler=_handle_status, format_hint="export_status")
26
+
27
+ get = export_subparsers.add_parser("get", help="获取导出结果")
28
+ get.add_argument("--export-handle", required=True)
29
+ get.add_argument("--download-to-path")
30
+ get.set_defaults(handler=_handle_get, format_hint="export_get")
31
+
32
+ direct = export_subparsers.add_parser("direct", help="直接导出并下载")
33
+ direct.add_argument("--app-key", required=True)
34
+ direct.add_argument("--view-id", default="system:all")
35
+ direct.add_argument("--column", dest="columns", action="append", type=int, default=[], help="只导出这些 field_id;不传时导出当前视图全部字段")
36
+ direct.add_argument("--columns-file", help="JSON/YAML list,内容与 --column 语义一致")
37
+ direct.add_argument("--record-id", dest="record_ids", action="append", default=[], help="只导出这些 record_id;不传时导出当前视图全部数据")
38
+ direct.add_argument("--record-ids-file", help="JSON/YAML list,内容与 --record-id 语义一致")
39
+ direct.add_argument("--include-workflow-log", action=argparse.BooleanOptionalAction, default=False, help="是否同时导出流程日志")
40
+ direct.add_argument("--download-to-path")
41
+ direct.add_argument("--wait-timeout-seconds", type=float)
42
+ direct.set_defaults(handler=_handle_direct, format_hint="export_direct")
43
+
44
+
45
+ def _columns(args: argparse.Namespace) -> list[int | dict]:
46
+ columns: list[int | dict] = list(args.columns or [])
47
+ if args.columns_file:
48
+ columns.extend(load_list_arg(args.columns_file, option_name="--columns-file"))
49
+ return columns
50
+
51
+
52
+ def _record_ids(args: argparse.Namespace) -> list[str | int]:
53
+ record_ids: list[str | int] = list(args.record_ids or [])
54
+ if args.record_ids_file:
55
+ record_ids.extend(load_list_arg(args.record_ids_file, option_name="--record-ids-file"))
56
+ return record_ids
57
+
58
+
59
+ def _handle_start(args: argparse.Namespace, context: CliContext) -> dict:
60
+ return context.exports.record_export_start(
61
+ profile=args.profile,
62
+ app_key=args.app_key,
63
+ view_id=args.view_id,
64
+ columns=_columns(args),
65
+ record_ids=_record_ids(args),
66
+ include_workflow_log=args.include_workflow_log,
67
+ )
68
+
69
+
70
+ def _handle_status(args: argparse.Namespace, context: CliContext) -> dict:
71
+ return context.exports.record_export_status_get(
72
+ profile=args.profile,
73
+ export_handle=args.export_handle,
74
+ )
75
+
76
+
77
+ def _handle_get(args: argparse.Namespace, context: CliContext) -> dict:
78
+ return context.exports.record_export_get(
79
+ profile=args.profile,
80
+ export_handle=args.export_handle,
81
+ download_to_path=args.download_to_path,
82
+ )
83
+
84
+
85
+ def _handle_direct(args: argparse.Namespace, context: CliContext) -> dict:
86
+ return context.exports.record_export_direct(
87
+ profile=args.profile,
88
+ app_key=args.app_key,
89
+ view_id=args.view_id,
90
+ columns=_columns(args),
91
+ record_ids=_record_ids(args),
92
+ include_workflow_log=args.include_workflow_log,
93
+ download_to_path=args.download_to_path,
94
+ wait_timeout_seconds=args.wait_timeout_seconds,
95
+ )
@@ -8,6 +8,7 @@ from ..tools.ai_builder_tools import AiBuilderTools
8
8
  from ..tools.app_tools import AppTools
9
9
  from ..tools.auth_tools import AuthTools
10
10
  from ..tools.code_block_tools import CodeBlockTools
11
+ from ..tools.export_tools import ExportTools
11
12
  from ..tools.feedback_tools import FeedbackTools
12
13
  from ..tools.file_tools import FileTools
13
14
  from ..tools.import_tools import ImportTools
@@ -29,6 +30,7 @@ class CliContext:
29
30
  record: RecordTools
30
31
  code_block: CodeBlockTools
31
32
  imports: ImportTools
33
+ exports: ExportTools
32
34
  task: TaskContextTools
33
35
  files: FileTools
34
36
  builder_feedback: FeedbackTools
@@ -52,6 +54,7 @@ def build_cli_context() -> CliContext:
52
54
  record=RecordTools(sessions, backend),
53
55
  code_block=CodeBlockTools(sessions, backend),
54
56
  imports=ImportTools(sessions, backend),
57
+ exports=ExportTools(sessions, backend),
55
58
  task=TaskContextTools(sessions, backend),
56
59
  files=FileTools(sessions, backend),
57
60
  builder_feedback=FeedbackTools(backend, mcp_side="App Builder MCP"),
@@ -412,6 +412,73 @@ def _format_import_status(result: dict[str, Any]) -> str:
412
412
  return "\n".join(lines) + "\n"
413
413
 
414
414
 
415
+ def _format_export_common(result: dict[str, Any], *, title: str | None = None) -> str:
416
+ lines: list[str] = []
417
+ if title:
418
+ lines.append(title)
419
+ lines.extend(
420
+ [
421
+ f"Status: {result.get('status') or '-'}",
422
+ f"Export Handle: {result.get('export_handle') or '-'}",
423
+ f"App Key: {result.get('app_key') or '-'}",
424
+ f"View ID: {result.get('view_id') or '-'}",
425
+ f"Process Status: {result.get('process_status') or '-'}",
426
+ f"Rows: {result.get('num') if result.get('num') is not None else '-'}",
427
+ ]
428
+ )
429
+ row_scope = result.get("row_scope")
430
+ if row_scope not in (None, ""):
431
+ lines.append(f"Row Scope: {row_scope}")
432
+ selected_record_count = result.get("selected_record_count")
433
+ if selected_record_count not in (None, ""):
434
+ lines.append(f"Selected Rows: {selected_record_count}")
435
+ field_scope = result.get("field_scope")
436
+ if field_scope not in (None, ""):
437
+ lines.append(f"Field Scope: {field_scope}")
438
+ selected_field_count = result.get("selected_field_count")
439
+ if selected_field_count not in (None, ""):
440
+ lines.append(f"Selected Fields: {selected_field_count}")
441
+ include_workflow_log = result.get("include_workflow_log")
442
+ if include_workflow_log not in (None, ""):
443
+ lines.append(f"Include Workflow Log: {include_workflow_log}")
444
+ file_names = result.get("file_names") if isinstance(result.get("file_names"), list) else []
445
+ file_urls = result.get("file_urls") if isinstance(result.get("file_urls"), list) else []
446
+ if file_names or file_urls:
447
+ lines.append("Remote Files:")
448
+ max_items = max(len(file_names), len(file_urls))
449
+ for index in range(max_items):
450
+ name = file_names[index] if index < len(file_names) else "-"
451
+ url = file_urls[index] if index < len(file_urls) else "-"
452
+ lines.append(f"- {name}: {url}")
453
+ downloaded_files = result.get("downloaded_files") if isinstance(result.get("downloaded_files"), list) else []
454
+ if downloaded_files:
455
+ lines.append("Downloaded Files:")
456
+ for item in downloaded_files:
457
+ if isinstance(item, dict):
458
+ lines.append(f"- {item.get('file_name') or '-'}: {item.get('path') or '-'}")
459
+ else:
460
+ lines.append(f"- {item}")
461
+ _append_warnings(lines, result.get("warnings"))
462
+ _append_verification(lines, result.get("verification"))
463
+ return "\n".join(lines) + "\n"
464
+
465
+
466
+ def _format_export_start(result: dict[str, Any]) -> str:
467
+ return _format_export_common(result, title="Export Accepted")
468
+
469
+
470
+ def _format_export_status(result: dict[str, Any]) -> str:
471
+ return _format_export_common(result, title="Export Status")
472
+
473
+
474
+ def _format_export_get(result: dict[str, Any]) -> str:
475
+ return _format_export_common(result, title="Export Result")
476
+
477
+
478
+ def _format_export_direct(result: dict[str, Any]) -> str:
479
+ return _format_export_common(result, title="Export Direct")
480
+
481
+
415
482
  def _format_builder_summary(result: dict[str, Any]) -> str:
416
483
  lines = []
417
484
  if "status" in result:
@@ -626,5 +693,9 @@ _FORMATTERS = {
626
693
  "task_associated_report_detail_get": _format_task_associated_report_detail,
627
694
  "import_verify": _format_import_verify,
628
695
  "import_status": _format_import_status,
696
+ "export_start": _format_export_start,
697
+ "export_status": _format_export_status,
698
+ "export_get": _format_export_get,
699
+ "export_direct": _format_export_direct,
629
700
  "builder_summary": _format_builder_summary,
630
701
  }
@@ -0,0 +1,14 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import timedelta
4
+ from pathlib import Path
5
+
6
+ from .import_store import _JsonEntryStore, _store_dir
7
+
8
+
9
+ class ExportJobStore(_JsonEntryStore):
10
+ def __init__(self, base_dir: Path | None = None, *, ttl_seconds: int = 24 * 3600) -> None:
11
+ super().__init__(
12
+ base_dir=base_dir or _store_dir("QINGFLOW_MCP_EXPORT_JOB_HOME", "export-jobs"),
13
+ ttl=timedelta(seconds=ttl_seconds),
14
+ )
@@ -91,6 +91,10 @@ USER_PUBLIC_TOOL_SPECS: tuple[PublicToolSpec, ...] = (
91
91
  PublicToolSpec(USER_DOMAIN, "record_import_repair_local", ("record_import_repair_local",), ("import", "repair")),
92
92
  PublicToolSpec(USER_DOMAIN, "record_import_start", ("record_import_start",), ("import", "start")),
93
93
  PublicToolSpec(USER_DOMAIN, "record_import_status_get", ("record_import_status_get",), ("import", "status")),
94
+ PublicToolSpec(USER_DOMAIN, "record_export_start", ("record_export_start",), ("export", "start"), cli_show_effective_context=True),
95
+ PublicToolSpec(USER_DOMAIN, "record_export_status_get", ("record_export_status_get",), ("export", "status"), cli_show_effective_context=True),
96
+ PublicToolSpec(USER_DOMAIN, "record_export_get", ("record_export_get",), ("export", "get"), cli_show_effective_context=True),
97
+ PublicToolSpec(USER_DOMAIN, "record_export_direct", ("record_export_direct",), ("export", "direct"), cli_show_effective_context=True),
94
98
  PublicToolSpec(USER_DOMAIN, "record_code_block_run", ("record_code_block_run",), ("record", "code-block-run"), cli_show_effective_context=True, cli_context_write=True),
95
99
  PublicToolSpec(USER_DOMAIN, "task_list", ("task_list",), ("task", "list"), cli_show_effective_context=True),
96
100
  PublicToolSpec(USER_DOMAIN, "task_get", ("task_get",), ("task", "get"), cli_show_effective_context=True),
@@ -674,6 +674,10 @@ def _trim_import_status_payload(payload: JSONObject) -> None:
674
674
  payload.pop(key, None)
675
675
 
676
676
 
677
+ def _trim_export_payload(payload: JSONObject) -> None:
678
+ payload.pop("backend_export_id", None)
679
+
680
+
677
681
  def _summarize_import_issues(issues: list[Any]) -> dict[str, Any]:
678
682
  total = 0
679
683
  error_count = 0
@@ -756,6 +760,16 @@ _register_policy(
756
760
  ),
757
761
  _trim_import_schema,
758
762
  )
763
+ _register_policy(
764
+ (USER_DOMAIN,),
765
+ (
766
+ "record_export_start",
767
+ "record_export_status_get",
768
+ "record_export_get",
769
+ "record_export_direct",
770
+ ),
771
+ _trim_export_payload,
772
+ )
759
773
  _register_policy(
760
774
  (USER_DOMAIN,),
761
775
  (
@@ -10,6 +10,7 @@ from .tools.app_tools import AppTools
10
10
  from .tools.auth_tools import AuthTools
11
11
  from .tools.code_block_tools import CodeBlockTools
12
12
  from .tools.feedback_tools import FeedbackTools
13
+ from .tools.export_tools import ExportTools
13
14
  from .tools.file_tools import FileTools
14
15
  from .tools.import_tools import ImportTools
15
16
  from .tools.package_tools import PackageTools
@@ -50,6 +51,7 @@ All resource tools operate with the logged-in user's Qingflow permissions.
50
51
  If `app_key` is unknown, use `app_list` or `app_search` first.
51
52
  If the app is known but the data range is not, use `app_get` first and choose from `accessible_views`.
52
53
  If an accessible view has `analysis_supported=false`, do not use it for `record_list` or `record_analyze`. `boardView` and `ganttView` are special UI views, not list/analyze targets.
54
+ `view_get(view_id=...)` also returns `export_capability`; it only means there is a supported export route, not that export permission has been verified.
53
55
 
54
56
  ## Schema-First Rule
55
57
 
@@ -147,6 +149,20 @@ Use `record_code_block_run` when the user wants to execute a form code-block fie
147
149
  - Do not modify user-uploaded files unless the user explicitly authorizes repair.
148
150
  - If repair is authorized, keep the original file and repair a copy, then run `record_import_verify` again before `record_import_start`.
149
151
 
152
+ ## Export Path
153
+
154
+ `view_get -> record_export_start -> record_export_status_get -> record_export_get`
155
+
156
+ - `record_export_direct` is the one-shot export path that starts the export, waits for completion, downloads locally, and still returns remote download links.
157
+ - Export v1 supports record views only and follows the same public `view_id` semantics as `record_list` (`system:*` and `custom:*`).
158
+ - `record_export_start` / `record_export_direct` support frontend-like row selection:
159
+ - omit `record_ids` to export all rows in the selected view
160
+ - pass `record_ids` to export selected rows only
161
+ - `record_export_start` / `record_export_direct` also support frontend-like column selection:
162
+ - omit `columns` to export all current-view fields
163
+ - pass `columns` to export only selected fields, preserving the provided order
164
+ - `include_workflow_log=true` maps to the native workflow-log export switch.
165
+
150
166
  ## Task Workflow Path
151
167
 
152
168
  `task_list -> task_get -> task_action_execute`
@@ -190,6 +206,7 @@ If the current MCP capability is unsupported, the workflow is awkward, or the us
190
206
  WorkspaceTools(sessions, backend).register(server)
191
207
  FileTools(sessions, backend).register(server)
192
208
  ImportTools(sessions, backend).register(server)
209
+ ExportTools(sessions, backend).register(server)
193
210
  CodeBlockTools(sessions, backend).register(server)
194
211
  TaskContextTools(sessions, backend).register(server)
195
212
  RoleTools(sessions, backend).register(server)
@@ -13,6 +13,7 @@ from .tools.app_tools import AppTools
13
13
  from .tools.auth_tools import AuthTools
14
14
  from .tools.code_block_tools import CodeBlockTools
15
15
  from .tools.directory_tools import DirectoryTools
16
+ from .tools.export_tools import ExportTools
16
17
  from .tools.feedback_tools import FeedbackTools
17
18
  from .tools.file_tools import FileTools
18
19
  from .tools.import_tools import ImportTools
@@ -33,6 +34,7 @@ def build_user_server() -> FastMCP:
33
34
  If `app_key` is unknown, use `app_list` or `app_search` first.
34
35
  If the app is known but the data range is not, use `app_get` first and choose from `accessible_views`.
35
36
  If an accessible view has `analysis_supported=false`, do not use it for `record_list` or `record_analyze`. `boardView` and `ganttView` are special UI views, not list/analyze targets.
37
+ `view_get(view_id=...)` also returns `export_capability`; it only means there is a supported export route, not that export permission has been verified.
36
38
 
37
39
  ## Shared Helper
38
40
 
@@ -142,6 +144,20 @@ Use `record_code_block_run` when the user wants to execute a form code-block fie
142
144
  - Do not modify user-uploaded files unless the user explicitly authorizes repair.
143
145
  - If repair is authorized, keep the original file and repair a copy, then run `record_import_verify` again before `record_import_start`.
144
146
 
147
+ ## Export Path
148
+
149
+ `view_get -> record_export_start -> record_export_status_get -> record_export_get`
150
+
151
+ - `record_export_direct` is the one-shot path that starts export, waits, downloads locally, and still returns remote download links.
152
+ - Export v1 supports record views only and follows the same public `view_id` semantics as `record_list`.
153
+ - `record_export_start` / `record_export_direct` support frontend-like row selection:
154
+ - omit `record_ids` to export all rows in the selected view
155
+ - pass `record_ids` to export selected rows only
156
+ - `record_export_start` / `record_export_direct` also support frontend-like column selection:
157
+ - omit `columns` to export all current-view fields
158
+ - pass `columns` to export only selected fields, preserving the provided order
159
+ - `include_workflow_log=true` maps to the native workflow-log export switch.
160
+
145
161
  ## Task Workflow Path
146
162
 
147
163
  `task_list -> task_get -> task_action_execute`
@@ -188,6 +204,7 @@ If the current MCP capability is unsupported, the workflow is awkward, or the us
188
204
  workspace = wrap_trimmed_methods(WorkspaceTools(sessions, backend), USER_SERVER_METHOD_MAP)
189
205
  file_tools = wrap_trimmed_methods(FileTools(sessions, backend), USER_SERVER_METHOD_MAP)
190
206
  imports = wrap_trimmed_methods(ImportTools(sessions, backend), USER_SERVER_METHOD_MAP)
207
+ exports = wrap_trimmed_methods(ExportTools(sessions, backend), USER_SERVER_METHOD_MAP)
191
208
  resources = wrap_trimmed_methods(ResourceReadTools(sessions, backend), USER_SERVER_METHOD_MAP)
192
209
  feedback = FeedbackTools(backend, mcp_side="App User MCP")
193
210
  code_block_tools = wrap_trimmed_methods(CodeBlockTools(sessions, backend), USER_SERVER_METHOD_MAP)
@@ -218,6 +235,65 @@ If the current MCP capability is unsupported, the workflow is awkward, or the us
218
235
  def auth_logout(profile: str = DEFAULT_PROFILE, forget_persisted: bool = False) -> dict:
219
236
  return auth.auth_logout(profile=profile, forget_persisted=forget_persisted)
220
237
 
238
+ @server.tool()
239
+ def record_export_start(
240
+ profile: str = DEFAULT_PROFILE,
241
+ app_key: str = "",
242
+ view_id: str = "system:all",
243
+ columns: list[dict | int] | None = None,
244
+ record_ids: list[str | int] | None = None,
245
+ include_workflow_log: bool = False,
246
+ ) -> dict:
247
+ return exports.record_export_start(
248
+ profile=profile,
249
+ app_key=app_key,
250
+ view_id=view_id,
251
+ columns=columns or [],
252
+ record_ids=record_ids or [],
253
+ include_workflow_log=include_workflow_log,
254
+ )
255
+
256
+ @server.tool()
257
+ def record_export_status_get(
258
+ profile: str = DEFAULT_PROFILE,
259
+ export_handle: str = "",
260
+ ) -> dict:
261
+ return exports.record_export_status_get(profile=profile, export_handle=export_handle)
262
+
263
+ @server.tool()
264
+ def record_export_get(
265
+ profile: str = DEFAULT_PROFILE,
266
+ export_handle: str = "",
267
+ download_to_path: str | None = None,
268
+ ) -> dict:
269
+ return exports.record_export_get(
270
+ profile=profile,
271
+ export_handle=export_handle,
272
+ download_to_path=download_to_path,
273
+ )
274
+
275
+ @server.tool()
276
+ def record_export_direct(
277
+ profile: str = DEFAULT_PROFILE,
278
+ app_key: str = "",
279
+ view_id: str = "system:all",
280
+ columns: list[dict | int] | None = None,
281
+ record_ids: list[str | int] | None = None,
282
+ include_workflow_log: bool = False,
283
+ download_to_path: str | None = None,
284
+ wait_timeout_seconds: float | None = None,
285
+ ) -> dict:
286
+ return exports.record_export_direct(
287
+ profile=profile,
288
+ app_key=app_key,
289
+ view_id=view_id,
290
+ columns=columns or [],
291
+ record_ids=record_ids or [],
292
+ include_workflow_log=include_workflow_log,
293
+ download_to_path=download_to_path,
294
+ wait_timeout_seconds=wait_timeout_seconds,
295
+ )
296
+
221
297
  @server.tool()
222
298
  def workspace_list(
223
299
  profile: str = DEFAULT_PROFILE,