@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 +2 -2
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/src/qingflow_mcp/__init__.py +1 -1
- package/src/qingflow_mcp/cli/commands/task.py +70 -21
- package/src/qingflow_mcp/cli/formatters.py +206 -20
- package/src/qingflow_mcp/cli/main.py +6 -1
- package/src/qingflow_mcp/public_surface.py +7 -1
- package/src/qingflow_mcp/server.py +4 -0
- package/src/qingflow_mcp/server_app_user.py +4 -0
- package/src/qingflow_mcp/tools/task_context_tools.py +333 -110
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.
|
|
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.
|
|
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
package/pyproject.toml
CHANGED
|
@@ -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("--
|
|
28
|
-
get.add_argument("--
|
|
29
|
-
get.add_argument("--
|
|
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("--
|
|
36
|
-
action.add_argument("--
|
|
37
|
-
action.add_argument("--
|
|
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(
|
|
43
|
+
action.set_defaults(
|
|
44
|
+
handler=_handle_action,
|
|
45
|
+
format_hint="task_action_execute",
|
|
46
|
+
hide_effective_context_line=True,
|
|
47
|
+
)
|
|
42
48
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
181
|
-
"
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
|
|
211
|
-
f"
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
|
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(
|
|
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`.
|