@josephyan/qingflow-app-user-mcp 0.2.0-beta.994 → 0.2.0-beta.996

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-user-mcp@0.2.0-beta.994
6
+ npm install @josephyan/qingflow-app-user-mcp@0.2.0-beta.996
7
7
  ```
8
8
 
9
9
  Run:
10
10
 
11
11
  ```bash
12
- npx -y -p @josephyan/qingflow-app-user-mcp@0.2.0-beta.994 qingflow-app-user-mcp
12
+ npx -y -p @josephyan/qingflow-app-user-mcp@0.2.0-beta.996 qingflow-app-user-mcp
13
13
  ```
14
14
 
15
15
  Environment:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@josephyan/qingflow-app-user-mcp",
3
- "version": "0.2.0-beta.994",
3
+ "version": "0.2.0-beta.996",
4
4
  "description": "Operational end-user MCP for Qingflow records, tasks, comments, and directory 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.0b994"
7
+ version = "0.2.0b996"
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.0b994"
8
+ _FALLBACK_VERSION = "0.2.0b996"
9
9
 
10
10
 
11
11
  def _resolve_local_pyproject_version() -> str | None:
@@ -23,27 +23,44 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
23
23
  list_parser.add_argument("--page-size", type=int, default=20)
24
24
  list_parser.set_defaults(handler=_handle_list, format_hint="task_list")
25
25
 
26
- get = task_subparsers.add_parser("get", help="读取待办详情")
27
- get.add_argument("--app-key", required=True)
28
- get.add_argument("--record-id", required=True)
29
- get.add_argument("--workflow-node-id", required=True, type=int)
26
+ get = task_subparsers.add_parser("get", help="读取待办详情;推荐直接传 --task-id")
27
+ get.add_argument("--task-id")
28
+ get.add_argument("--app-key")
29
+ get.add_argument("--record-id")
30
+ get.add_argument("--workflow-node-id", type=int)
30
31
  get.add_argument("--include-candidates", action=argparse.BooleanOptionalAction, default=True)
31
32
  get.add_argument("--include-associated-reports", action=argparse.BooleanOptionalAction, default=True)
32
33
  get.set_defaults(handler=_handle_get, format_hint="task_get")
33
34
 
34
35
  action = task_subparsers.add_parser("action", help="执行待办动作")
35
- action.add_argument("--app-key", required=True)
36
- action.add_argument("--record-id", required=True)
37
- action.add_argument("--workflow-node-id", required=True, type=int)
36
+ action.add_argument("--task-id")
37
+ action.add_argument("--app-key")
38
+ action.add_argument("--record-id")
39
+ action.add_argument("--workflow-node-id", type=int)
38
40
  action.add_argument("--action", required=True)
39
41
  action.add_argument("--payload-file")
40
42
  action.add_argument("--fields-file")
41
- action.set_defaults(handler=_handle_action, format_hint="")
43
+ action.set_defaults(
44
+ handler=_handle_action,
45
+ format_hint="task_action_execute",
46
+ hide_effective_context_line=True,
47
+ )
42
48
 
43
- log = task_subparsers.add_parser("log", help="读取流程日志")
44
- log.add_argument("--app-key", required=True)
45
- log.add_argument("--record-id", required=True)
46
- log.add_argument("--workflow-node-id", required=True, type=int)
49
+ report = task_subparsers.add_parser("report", help="读取待办关联报表详情;推荐直接传 --task-id")
50
+ report.add_argument("--task-id")
51
+ report.add_argument("--app-key")
52
+ report.add_argument("--record-id")
53
+ report.add_argument("--workflow-node-id", type=int)
54
+ report.add_argument("--report-id", required=True, type=int)
55
+ report.add_argument("--page", type=int, default=1)
56
+ report.add_argument("--page-size", type=int, default=20)
57
+ report.set_defaults(handler=_handle_report, format_hint="task_associated_report_detail_get")
58
+
59
+ log = task_subparsers.add_parser("log", help="读取流程日志;推荐直接传 --task-id")
60
+ log.add_argument("--task-id")
61
+ log.add_argument("--app-key")
62
+ log.add_argument("--record-id")
63
+ log.add_argument("--workflow-node-id", type=int)
47
64
  log.set_defaults(handler=_handle_log, format_hint="")
48
65
 
49
66
 
@@ -61,22 +78,32 @@ def _handle_list(args: argparse.Namespace, context: CliContext) -> dict:
61
78
 
62
79
 
63
80
  def _handle_get(args: argparse.Namespace, context: CliContext) -> dict:
81
+ if not args.task_id and not (args.app_key and args.record_id and args.workflow_node_id):
82
+ raise RuntimeError(
83
+ '{"category":"config","message":"task get requires --task-id, or --app-key together with --record-id and --workflow-node-id"}'
84
+ )
64
85
  return context.task.task_get(
65
86
  profile=args.profile,
66
- app_key=args.app_key,
67
- record_id=args.record_id,
68
- workflow_node_id=args.workflow_node_id,
87
+ task_id=args.task_id,
88
+ app_key=args.app_key or "",
89
+ record_id=args.record_id or "",
90
+ workflow_node_id=int(args.workflow_node_id or 0),
69
91
  include_candidates=bool(args.include_candidates),
70
92
  include_associated_reports=bool(args.include_associated_reports),
71
93
  )
72
94
 
73
95
 
74
96
  def _handle_action(args: argparse.Namespace, context: CliContext) -> dict:
97
+ if not args.task_id and not (args.app_key and args.record_id and args.workflow_node_id):
98
+ raise RuntimeError(
99
+ '{"category":"config","message":"task action requires --task-id, or --app-key together with --record-id and --workflow-node-id"}'
100
+ )
75
101
  return context.task.task_action_execute(
76
102
  profile=args.profile,
77
- app_key=args.app_key,
78
- record_id=args.record_id,
79
- workflow_node_id=args.workflow_node_id,
103
+ task_id=args.task_id,
104
+ app_key=args.app_key or "",
105
+ record_id=args.record_id or "",
106
+ workflow_node_id=int(args.workflow_node_id or 0),
80
107
  action=args.action,
81
108
  payload=load_object_arg(args.payload_file, option_name="--payload-file") or {},
82
109
  fields=load_object_arg(args.fields_file, option_name="--fields-file") or {},
@@ -84,9 +111,31 @@ def _handle_action(args: argparse.Namespace, context: CliContext) -> dict:
84
111
 
85
112
 
86
113
  def _handle_log(args: argparse.Namespace, context: CliContext) -> dict:
114
+ if not args.task_id and not (args.app_key and args.record_id and args.workflow_node_id):
115
+ raise RuntimeError(
116
+ '{"category":"config","message":"task log requires --task-id, or --app-key together with --record-id and --workflow-node-id"}'
117
+ )
87
118
  return context.task.task_workflow_log_get(
88
119
  profile=args.profile,
89
- app_key=args.app_key,
90
- record_id=args.record_id,
91
- workflow_node_id=args.workflow_node_id,
120
+ task_id=args.task_id,
121
+ app_key=args.app_key or "",
122
+ record_id=args.record_id or "",
123
+ workflow_node_id=int(args.workflow_node_id or 0),
124
+ )
125
+
126
+
127
+ def _handle_report(args: argparse.Namespace, context: CliContext) -> dict:
128
+ if not args.task_id and not (args.app_key and args.record_id and args.workflow_node_id):
129
+ raise RuntimeError(
130
+ '{"category":"config","message":"task report requires --task-id, or --app-key together with --record-id and --workflow-node-id"}'
131
+ )
132
+ return context.task.task_associated_report_detail_get(
133
+ profile=args.profile,
134
+ task_id=args.task_id,
135
+ app_key=args.app_key or "",
136
+ record_id=args.record_id or "",
137
+ workflow_node_id=int(args.workflow_node_id or 0),
138
+ report_id=int(args.report_id),
139
+ page=int(args.page),
140
+ page_size=int(args.page_size),
92
141
  )
@@ -177,17 +177,15 @@ def _format_task_list(result: dict[str, Any]) -> str:
177
177
  for item in items:
178
178
  if not isinstance(item, dict):
179
179
  continue
180
- lines.append(
181
- "- "
182
- + " / ".join(
183
- [
184
- str(item.get("app_key") or "-"),
185
- str(item.get("record_id") or "-"),
186
- str(item.get("workflow_node_id") or "-"),
187
- str(item.get("workflow_node_name") or "-"),
188
- ]
189
- )
190
- )
180
+ header_parts = [
181
+ str(item.get("task_id") or "-"),
182
+ str(item.get("app_name") or item.get("app_key") or "-"),
183
+ str(item.get("workflow_node_name") or "-"),
184
+ ]
185
+ apply_time = item.get("apply_time")
186
+ if apply_time not in (None, ""):
187
+ header_parts.append(str(apply_time))
188
+ lines.append("- " + " / ".join(header_parts))
191
189
  summary_fields = item.get("summary_fields") if isinstance(item.get("summary_fields"), list) else []
192
190
  for summary in summary_fields:
193
191
  if not isinstance(summary, dict):
@@ -206,15 +204,20 @@ def _format_task_get(result: dict[str, Any]) -> str:
206
204
  extras = data.get("extras") if isinstance(data.get("extras"), dict) else {}
207
205
  initiator = task.get("initiator") if isinstance(task.get("initiator"), dict) else {}
208
206
  initiator_label = initiator.get("displayName") or initiator.get("email") or "-"
209
- lines = [
210
- f"Task: {task.get('app_key') or '-'} / {task.get('record_id') or '-'} / {task.get('workflow_node_id') or '-'}",
211
- f"Node: {task.get('workflow_node_name') or '-'}",
212
- f"App: {task.get('app_name') or '-'}",
213
- f"Initiator: {initiator_label}",
214
- f"Apply Status: {record_summary.get('apply_status')}",
215
- f"Available Actions: {', '.join(str(item) for item in available_actions) or '-'}",
216
- f"Editable Fields: {len(editable_fields)}",
217
- ]
207
+ lines = []
208
+ if task.get("task_id") not in (None, ""):
209
+ lines.append(f"Task ID: {task.get('task_id')}")
210
+ lines.extend(
211
+ [
212
+ f"Locator: {task.get('app_key') or '-'} / {task.get('record_id') or '-'} / {task.get('workflow_node_id') or '-'}",
213
+ f"Node: {task.get('workflow_node_name') or '-'}",
214
+ f"App: {task.get('app_name') or '-'}",
215
+ f"Initiator: {initiator_label}",
216
+ f"Apply Status: {record_summary.get('apply_status')}",
217
+ f"Available Actions: {', '.join(str(item) for item in available_actions) or '-'}",
218
+ f"Editable Fields: {len(editable_fields)}",
219
+ ]
220
+ )
218
221
  core_fields = record_summary.get("core_fields") if isinstance(record_summary.get("core_fields"), dict) else {}
219
222
  if core_fields:
220
223
  lines.append("Core Fields:")
@@ -246,6 +249,72 @@ def _format_task_get(result: dict[str, Any]) -> str:
246
249
  return "\n".join(lines) + "\n"
247
250
 
248
251
 
252
+ def _format_task_action(result: dict[str, Any]) -> str:
253
+ data = result.get("data") if isinstance(result.get("data"), dict) else {}
254
+ action = str(data.get("action") or "").strip().lower()
255
+ status = str(result.get("status") or "").strip().lower()
256
+
257
+ if status == "failed" or result.get("ok") is False:
258
+ lines = [_task_action_failure_label(action)]
259
+ reason = _task_action_failure_reason(result)
260
+ if reason:
261
+ lines.append(f"原因:{reason}")
262
+ debug_lines = _task_action_debug_lines(result)
263
+ if debug_lines:
264
+ lines.append("调试信息:")
265
+ lines.extend(f"- {line}" for line in debug_lines)
266
+ return "\n".join(lines) + "\n"
267
+
268
+ if status == "partial_success":
269
+ lines = [_task_action_success_label(action)]
270
+ lines.append(f"说明:{_task_action_partial_success_message(result)}")
271
+ return "\n".join(lines) + "\n"
272
+
273
+ return _task_action_success_label(action) + "\n"
274
+
275
+
276
+ def _format_task_associated_report_detail(result: dict[str, Any]) -> str:
277
+ data = result.get("data") if isinstance(result.get("data"), dict) else {}
278
+ selection = data.get("selection") if isinstance(data.get("selection"), dict) else {}
279
+ result_type = str(data.get("result_type") or "-")
280
+ context = data.get("context") if isinstance(data.get("context"), dict) else {}
281
+ lines = []
282
+ if selection.get("task_id") not in (None, ""):
283
+ lines.append(f"Task ID: {selection.get('task_id')}")
284
+ lines.extend(
285
+ [
286
+ f"Report: {selection.get('chart_name') or '-'} ({selection.get('report_id') or '-'})",
287
+ f"Type: {result_type}",
288
+ ]
289
+ )
290
+ if result_type == "view_list":
291
+ result_payload = data.get("result") if isinstance(data.get("result"), dict) else {}
292
+ items = result_payload.get("items") if isinstance(result_payload.get("items"), list) else []
293
+ lines.append(f"Returned Records: {len(items)}")
294
+ for item in items[:10]:
295
+ if isinstance(item, dict):
296
+ lines.append(json.dumps(item, ensure_ascii=False))
297
+ if len(items) > 10:
298
+ lines.append(f"... {len(items) - 10} more")
299
+ elif result_type == "chart_data":
300
+ result_payload = data.get("result") if isinstance(data.get("result"), dict) else {}
301
+ summary = result_payload.get("summary") if isinstance(result_payload.get("summary"), dict) else {}
302
+ rows = result_payload.get("rows") if isinstance(result_payload.get("rows"), list) else []
303
+ if summary:
304
+ lines.append(f"Summary: {json.dumps(summary, ensure_ascii=False)}")
305
+ lines.append(f"Rows: {len(rows)}")
306
+ for row in rows[:10]:
307
+ if isinstance(row, dict):
308
+ lines.append(json.dumps(row, ensure_ascii=False))
309
+ if len(rows) > 10:
310
+ lines.append(f"... {len(rows) - 10} more")
311
+ resolved_filters = context.get("resolved_filters") if isinstance(context.get("resolved_filters"), list) else []
312
+ if resolved_filters:
313
+ lines.append(f"Resolved Filters: {len(resolved_filters)}")
314
+ _append_warnings(lines, result.get("warnings"))
315
+ return "\n".join(lines) + "\n"
316
+
317
+
249
318
  def _format_import_verify(result: dict[str, Any]) -> str:
250
319
  lines = [
251
320
  f"App Key: {result.get('app_key') or '-'}",
@@ -371,6 +440,121 @@ def _first_present(payload: dict[str, Any], *keys: str) -> Any:
371
440
  return None
372
441
 
373
442
 
443
+ def _task_action_success_label(action: str) -> str:
444
+ return {
445
+ "approve": "已通过",
446
+ "reject": "已驳回",
447
+ "rollback": "已退回",
448
+ "transfer": "已转交",
449
+ "save_only": "已保存",
450
+ "urge": "已催办",
451
+ }.get(action, "已执行")
452
+
453
+
454
+ def _task_action_failure_label(action: str) -> str:
455
+ return {
456
+ "approve": "审批失败",
457
+ "reject": "驳回失败",
458
+ "rollback": "退回失败",
459
+ "transfer": "转交失败",
460
+ "save_only": "保存失败",
461
+ "urge": "催办失败",
462
+ }.get(action, "执行失败")
463
+
464
+
465
+ def _task_action_partial_success_message(result: dict[str, Any]) -> str:
466
+ error_code = str(result.get("error_code") or "").strip().upper()
467
+ if error_code == "WORKFLOW_CONTINUATION_UNVERIFIED":
468
+ return "动作已提交,但暂未完成后续流程验证。可使用 --json 查看详细信息。"
469
+ if error_code == "TASK_ALREADY_PROCESSED":
470
+ return "当前待办已不可操作,系统判断流程可能已被其他人处理。可使用 --json 查看详细信息。"
471
+ warnings = result.get("warnings")
472
+ if isinstance(warnings, list):
473
+ for warning in warnings:
474
+ if not isinstance(warning, dict):
475
+ continue
476
+ code = str(warning.get("code") or "").strip().upper()
477
+ if code == "TASK_ALREADY_PROCESSED_UNCONFIRMED_ACTOR":
478
+ return "当前待办已不可操作,系统判断流程可能已被其他人处理。可使用 --json 查看详细信息。"
479
+ if code == "WORKFLOW_CONTINUATION_UNVERIFIED":
480
+ return "动作已提交,但暂未完成后续流程验证。可使用 --json 查看详细信息。"
481
+ return "动作已提交,但结果验证不完整。可使用 --json 查看详细信息。"
482
+
483
+
484
+ def _task_action_failure_reason(result: dict[str, Any]) -> str | None:
485
+ error_code = str(result.get("error_code") or "").strip().upper()
486
+ mapped_error = {
487
+ "TASK_CONTEXT_VISIBILITY_UNVERIFIED": "当前待办已不可操作,且系统未能确认是否已被处理。",
488
+ "TASK_SAVE_ONLY_VERIFICATION_FAILED": "保存请求已发送,但未能确认字段是否全部保存成功。",
489
+ "WORKFLOW_CONTINUATION_UNVERIFIED": "动作已提交,但暂未验证到流程继续推进。",
490
+ "TASK_ALREADY_PROCESSED": "当前待办已不可操作,系统判断流程可能已被其他人处理。",
491
+ }.get(error_code)
492
+ if mapped_error:
493
+ return mapped_error
494
+
495
+ warnings = result.get("warnings")
496
+ if isinstance(warnings, list):
497
+ for warning in warnings:
498
+ if isinstance(warning, dict) and warning.get("message"):
499
+ return str(warning.get("message"))
500
+
501
+ data = result.get("data") if isinstance(result.get("data"), dict) else {}
502
+ transport_error = data.get("transport_error") if isinstance(data.get("transport_error"), dict) else {}
503
+ backend_code = transport_error.get("backend_code")
504
+ http_status = transport_error.get("http_status")
505
+ if backend_code not in (None, ""):
506
+ return f"后端返回错误码 {backend_code}。"
507
+ if http_status not in (None, ""):
508
+ return f"请求返回 HTTP {http_status}。"
509
+
510
+ verification = result.get("verification") if isinstance(result.get("verification"), dict) else {}
511
+ record_state_error = verification.get("record_state_error") if isinstance(verification.get("record_state_error"), dict) else {}
512
+ backend_code = record_state_error.get("backend_code")
513
+ http_status = record_state_error.get("http_status")
514
+ if backend_code not in (None, ""):
515
+ return f"后端返回错误码 {backend_code}。"
516
+ if http_status not in (None, ""):
517
+ return f"请求返回 HTTP {http_status}。"
518
+ if error_code:
519
+ return f"错误码:{error_code}"
520
+ return None
521
+
522
+
523
+ def _task_action_debug_lines(result: dict[str, Any]) -> list[str]:
524
+ lines: list[str] = []
525
+ error_code = result.get("error_code")
526
+ if error_code not in (None, ""):
527
+ lines.append(f"error_code: {error_code}")
528
+
529
+ data = result.get("data") if isinstance(result.get("data"), dict) else {}
530
+ transport_error = data.get("transport_error") if isinstance(data.get("transport_error"), dict) else {}
531
+ for key in ("backend_code", "http_status", "category"):
532
+ value = transport_error.get(key)
533
+ if value not in (None, ""):
534
+ lines.append(f"{key}: {value}")
535
+
536
+ verification = result.get("verification") if isinstance(result.get("verification"), dict) else {}
537
+ for key in (
538
+ "runtime_continuation_verified",
539
+ "task_context_visibility_verified",
540
+ "fields_saved_verified",
541
+ "task_still_actionable",
542
+ "workflow_not_advanced",
543
+ "record_state_readable",
544
+ ):
545
+ if key in verification and verification.get(key) is not None:
546
+ lines.append(f"{key}: {verification.get(key)}")
547
+
548
+ record_state_error = verification.get("record_state_error") if isinstance(verification.get("record_state_error"), dict) else {}
549
+ for key in ("backend_code", "http_status", "category"):
550
+ value = record_state_error.get(key)
551
+ if value not in (None, ""):
552
+ entry = f"record_state_{key}: {value}"
553
+ if entry not in lines:
554
+ lines.append(entry)
555
+ return lines
556
+
557
+
374
558
  _FORMATTERS = {
375
559
  "auth_whoami": _format_whoami,
376
560
  "workspace_list": _format_workspace_list,
@@ -381,6 +565,8 @@ _FORMATTERS = {
381
565
  "record_list": _format_record_list,
382
566
  "task_list": _format_task_list,
383
567
  "task_get": _format_task_get,
568
+ "task_action_execute": _format_task_action,
569
+ "task_associated_report_detail_get": _format_task_associated_report_detail,
384
570
  "import_verify": _format_import_verify,
385
571
  "import_status": _format_import_status,
386
572
  "builder_summary": _format_builder_summary,
@@ -152,6 +152,7 @@ def _emit_cli_effective_context_notice(args: argparse.Namespace, context: CliCon
152
152
  spec = cli_public_tool_spec_from_namespace(args)
153
153
  if spec is None or not spec.cli_show_effective_context:
154
154
  return
155
+ hide_context_line = bool(getattr(args, "hide_effective_context_line", False))
155
156
  sessions = getattr(context, "sessions", None)
156
157
  if sessions is None or not hasattr(sessions, "get_profile"):
157
158
  return
@@ -168,9 +169,13 @@ def _emit_cli_effective_context_notice(args: argparse.Namespace, context: CliCon
168
169
  workspace_label = f"{workspace_name} ({workspace_id})"
169
170
  else:
170
171
  workspace_label = str(workspace_id)
171
- lines = [f"Context: profile={profile_name} workspace={workspace_label}"]
172
+ lines: list[str] = []
173
+ if not hide_context_line:
174
+ lines.append(f"Context: profile={profile_name} workspace={workspace_label}")
172
175
  if spec.cli_context_write and profile_name == "default":
173
176
  lines.append("Warning: using default profile for a workspace-sensitive write command")
177
+ if not lines:
178
+ return
174
179
  stream.write("\n".join(lines) + "\n")
175
180
 
176
181
 
@@ -94,7 +94,13 @@ USER_PUBLIC_TOOL_SPECS: tuple[PublicToolSpec, ...] = (
94
94
  PublicToolSpec(USER_DOMAIN, "task_list", ("task_list",), ("task", "list"), cli_show_effective_context=True),
95
95
  PublicToolSpec(USER_DOMAIN, "task_get", ("task_get",), ("task", "get"), cli_show_effective_context=True),
96
96
  PublicToolSpec(USER_DOMAIN, "task_action_execute", ("task_action_execute",), ("task", "action"), cli_show_effective_context=True, cli_context_write=True),
97
- PublicToolSpec(USER_DOMAIN, "task_associated_report_detail_get", ("task_associated_report_detail_get",), cli_public=False, cli_show_effective_context=True),
97
+ PublicToolSpec(
98
+ USER_DOMAIN,
99
+ "task_associated_report_detail_get",
100
+ ("task_associated_report_detail_get",),
101
+ ("task", "report"),
102
+ cli_show_effective_context=True,
103
+ ),
98
104
  PublicToolSpec(USER_DOMAIN, "task_workflow_log_get", ("task_workflow_log_get",), ("task", "log"), cli_show_effective_context=True),
99
105
  PublicToolSpec(USER_DOMAIN, "directory_search", ("directory_search",), cli_public=False),
100
106
  PublicToolSpec(USER_DOMAIN, "directory_list_internal_users", ("directory_list_internal_users",), cli_public=False),
@@ -151,6 +151,10 @@ Use `record_code_block_run` when the user wants to execute a form code-block fie
151
151
 
152
152
  `task_list -> task_get -> task_action_execute`
153
153
 
154
+ - `task_list` returns task-card summaries keyed by `task_id`.
155
+ - Prefer `task_get(task_id=...)` for detail reads; MCP resolves the current todo locator internally.
156
+ - `task_action_execute(task_id=..., action=...)` is also supported; MCP resolves the current todo locator internally before calling the real action route.
157
+ - `task_workflow_log_get(task_id=...)` and `task_associated_report_detail_get(task_id=...)` are also supported for the current todo context.
154
158
  - Use `task_associated_report_detail_get` for associated view or report details.
155
159
  - Use `task_workflow_log_get` for full workflow log history.
156
160
  - Task actions operate on `app_key + record_id + workflow_node_id`, not `task_id`.
@@ -146,6 +146,10 @@ Use `record_code_block_run` when the user wants to execute a form code-block fie
146
146
 
147
147
  `task_list -> task_get -> task_action_execute`
148
148
 
149
+ - `task_list` returns task-card summaries keyed by `task_id`.
150
+ - Prefer `task_get(task_id=...)` for detail reads; MCP resolves the current todo locator internally.
151
+ - `task_action_execute(task_id=..., action=...)` is also supported; MCP resolves the current todo locator internally before calling the real action route.
152
+ - `task_workflow_log_get(task_id=...)` and `task_associated_report_detail_get(task_id=...)` are also supported for the current todo context.
149
153
  - Use `task_associated_report_detail_get` for associated view or report details.
150
154
  - Use `task_workflow_log_get` for full workflow log history.
151
155
  - Task actions operate on `app_key + record_id + workflow_node_id`, not `task_id`.