@qingflow-tech/qingflow-app-builder-mcp 1.0.1 → 1.0.3

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.
Files changed (42) hide show
  1. package/README.md +2 -2
  2. package/docs/local-agent-install.md +9 -3
  3. package/npm/lib/runtime.mjs +10 -3
  4. package/package.json +1 -1
  5. package/pyproject.toml +1 -1
  6. package/skills/qingflow-app-builder/SKILL.md +88 -184
  7. package/skills/qingflow-app-builder/references/create-app.md +15 -34
  8. package/skills/qingflow-app-builder/references/gotchas.md +3 -3
  9. package/skills/qingflow-app-builder/references/solution-playbooks.md +1 -2
  10. package/skills/qingflow-app-builder/references/tool-selection.md +9 -10
  11. package/src/qingflow_mcp/__init__.py +33 -1
  12. package/src/qingflow_mcp/builder_facade/models.py +14 -4
  13. package/src/qingflow_mcp/builder_facade/service.py +1582 -124
  14. package/src/qingflow_mcp/cli/commands/auth.py +69 -1
  15. package/src/qingflow_mcp/cli/commands/builder.py +4 -3
  16. package/src/qingflow_mcp/cli/commands/record.py +5 -5
  17. package/src/qingflow_mcp/cli/commands/task.py +74 -22
  18. package/src/qingflow_mcp/cli/commands/workspace.py +22 -0
  19. package/src/qingflow_mcp/cli/formatters.py +287 -48
  20. package/src/qingflow_mcp/cli/main.py +6 -1
  21. package/src/qingflow_mcp/cli/qingflow_login.py +116 -0
  22. package/src/qingflow_mcp/config.py +8 -0
  23. package/src/qingflow_mcp/errors.py +2 -2
  24. package/src/qingflow_mcp/id_utils.py +49 -0
  25. package/src/qingflow_mcp/public_surface.py +11 -1
  26. package/src/qingflow_mcp/response_trim.py +380 -9
  27. package/src/qingflow_mcp/server.py +4 -0
  28. package/src/qingflow_mcp/server_app_builder.py +11 -1
  29. package/src/qingflow_mcp/server_app_user.py +24 -0
  30. package/src/qingflow_mcp/session_store.py +69 -15
  31. package/src/qingflow_mcp/solution/compiler/form_compiler.py +2 -2
  32. package/src/qingflow_mcp/solution/executor.py +2 -2
  33. package/src/qingflow_mcp/tools/ai_builder_tools.py +48 -18
  34. package/src/qingflow_mcp/tools/app_tools.py +1 -0
  35. package/src/qingflow_mcp/tools/auth_tools.py +271 -12
  36. package/src/qingflow_mcp/tools/base.py +6 -2
  37. package/src/qingflow_mcp/tools/code_block_tools.py +2 -2
  38. package/src/qingflow_mcp/tools/import_tools.py +36 -2
  39. package/src/qingflow_mcp/tools/record_tools.py +410 -156
  40. package/src/qingflow_mcp/tools/resource_read_tools.py +114 -32
  41. package/src/qingflow_mcp/tools/task_context_tools.py +899 -141
  42. package/src/qingflow_mcp/tools/workspace_tools.py +141 -0
@@ -30,10 +30,13 @@ def tool_key(domain: str, tool_name: str) -> str:
30
30
 
31
31
 
32
32
  USER_PUBLIC_TOOL_SPECS: tuple[PublicToolSpec, ...] = (
33
+ PublicToolSpec(USER_DOMAIN, "auth_login", cli_route=("auth", "login"), mcp_public=False),
33
34
  PublicToolSpec(USER_DOMAIN, "auth_use_credential", ("auth_use_credential",), ("auth", "use-credential")),
34
35
  PublicToolSpec(USER_DOMAIN, "auth_whoami", ("auth_whoami",), ("auth", "whoami")),
35
36
  PublicToolSpec(USER_DOMAIN, "auth_logout", ("auth_logout",), ("auth", "logout")),
36
37
  PublicToolSpec(USER_DOMAIN, "workspace_list", ("workspace_list",), ("workspace", "list")),
38
+ PublicToolSpec(USER_DOMAIN, "workspace_get", ("workspace_get",), ("workspace", "get")),
39
+ PublicToolSpec(USER_DOMAIN, "workspace_select", ("workspace_select",), ("workspace", "select")),
37
40
  PublicToolSpec(USER_DOMAIN, "app_list", ("app_list",), ("app", "list"), cli_show_effective_context=True),
38
41
  PublicToolSpec(USER_DOMAIN, "app_search", ("app_search",), ("app", "search"), cli_show_effective_context=True),
39
42
  PublicToolSpec(USER_DOMAIN, "app_get", ("app_get",), ("app", "get"), cli_show_effective_context=True),
@@ -92,7 +95,13 @@ USER_PUBLIC_TOOL_SPECS: tuple[PublicToolSpec, ...] = (
92
95
  PublicToolSpec(USER_DOMAIN, "task_list", ("task_list",), ("task", "list"), cli_show_effective_context=True),
93
96
  PublicToolSpec(USER_DOMAIN, "task_get", ("task_get",), ("task", "get"), cli_show_effective_context=True),
94
97
  PublicToolSpec(USER_DOMAIN, "task_action_execute", ("task_action_execute",), ("task", "action"), cli_show_effective_context=True, cli_context_write=True),
95
- PublicToolSpec(USER_DOMAIN, "task_associated_report_detail_get", ("task_associated_report_detail_get",), cli_public=False, cli_show_effective_context=True),
98
+ PublicToolSpec(
99
+ USER_DOMAIN,
100
+ "task_associated_report_detail_get",
101
+ ("task_associated_report_detail_get",),
102
+ ("task", "report"),
103
+ cli_show_effective_context=True,
104
+ ),
96
105
  PublicToolSpec(USER_DOMAIN, "task_workflow_log_get", ("task_workflow_log_get",), ("task", "log"), cli_show_effective_context=True),
97
106
  PublicToolSpec(USER_DOMAIN, "directory_search", ("directory_search",), cli_public=False),
98
107
  PublicToolSpec(USER_DOMAIN, "directory_list_internal_users", ("directory_list_internal_users",), cli_public=False),
@@ -109,6 +118,7 @@ BUILDER_PUBLIC_TOOL_SPECS: tuple[PublicToolSpec, ...] = (
109
118
  PublicToolSpec(BUILDER_DOMAIN, "auth_whoami", ("auth_whoami",), ("builder", "auth", "whoami"), cli_public=False),
110
119
  PublicToolSpec(BUILDER_DOMAIN, "auth_logout", ("auth_logout",), ("builder", "auth", "logout"), cli_public=False),
111
120
  PublicToolSpec(BUILDER_DOMAIN, "workspace_list", ("workspace_list",), ("builder", "workspace", "list"), cli_public=False),
121
+ PublicToolSpec(BUILDER_DOMAIN, "workspace_get", ("workspace_get",), ("builder", "workspace", "get"), cli_public=False),
112
122
  PublicToolSpec(BUILDER_DOMAIN, "file_upload_local", ("file_upload_local",), ("builder", "file", "upload-local"), has_contract=True, cli_show_effective_context=True, cli_context_write=True),
113
123
  PublicToolSpec(BUILDER_DOMAIN, "feedback_submit", ("feedback_submit",), ("builder", "feedback", "submit"), has_contract=True),
114
124
  PublicToolSpec(BUILDER_DOMAIN, "builder_tool_contract", ("builder_tool_contract",), ("builder", "contract"), has_contract=False),
@@ -263,6 +263,15 @@ def _trim_workspace_list(payload: JSONObject) -> None:
263
263
  _trim_item_list(page, "list", allowed=("wsId", "workspaceName", "remark"))
264
264
 
265
265
 
266
+ def _trim_workspace_get(payload: JSONObject) -> None:
267
+ workspace = payload.get("workspace")
268
+ if isinstance(workspace, dict):
269
+ payload["workspace"] = _pick(
270
+ workspace,
271
+ allowed=("wsId", "workspaceName", "remark", "systemVersion", "auth"),
272
+ )
273
+
274
+
266
275
  def _trim_app_search_like(payload: JSONObject) -> None:
267
276
  payload.pop("apps", None)
268
277
  _trim_item_list(payload, "items", allowed=("app_key", "app_name", "package_name"))
@@ -287,34 +296,175 @@ def _trim_file_upload_local(payload: JSONObject) -> None:
287
296
 
288
297
 
289
298
  def _trim_import_schema(payload: JSONObject) -> None:
290
- pass
299
+ columns: list[JSONObject] | None = None
300
+ if isinstance(payload.get("columns"), list):
301
+ columns = [item for item in payload.get("columns", []) if isinstance(item, dict)]
302
+ elif isinstance(payload.get("expected_columns"), list):
303
+ columns = [item for item in payload.get("expected_columns", []) if isinstance(item, dict)]
304
+ if columns is not None:
305
+ payload["columns"] = [_compact_import_column(item) for item in columns]
306
+ payload.pop("expected_columns", None)
307
+ payload.pop("schema_fingerprint", None)
308
+ payload.pop("import_capability", None)
309
+ payload.pop("request_route", None)
310
+ payload.pop("verification", None)
311
+
312
+ if _looks_like_import_verify(payload):
313
+ _trim_import_verify_payload(payload)
314
+ return
315
+ if "applied_repairs" in payload or "repaired_file_path" in payload:
316
+ _trim_import_repair_payload(payload)
317
+ return
318
+ if "template_url" in payload or "downloaded_to_path" in payload:
319
+ _trim_import_template_payload(payload)
320
+ return
321
+ if "import_id" in payload or "process_id_str" in payload:
322
+ _trim_import_status_payload(payload)
323
+ return
291
324
 
292
325
 
293
326
  def _trim_record_schema(payload: JSONObject) -> None:
294
327
  payload.pop("legacy_schema", None)
328
+ template_map = payload.get("payload_template")
329
+ if not isinstance(template_map, dict):
330
+ template_map = None
331
+
332
+ if "writable_fields" in payload:
333
+ writable_fields = payload.get("writable_fields")
334
+ payload.pop("writable_fields", None)
335
+ required_fields: list[JSONObject] = []
336
+ optional_fields: list[JSONObject] = []
337
+ if isinstance(writable_fields, list):
338
+ for item in writable_fields:
339
+ compact = _compact_schema_field(item, template_map=template_map)
340
+ if not compact:
341
+ continue
342
+ if compact.get("required") is True:
343
+ required_fields.append(compact)
344
+ else:
345
+ optional_fields.append(compact)
346
+ payload["required_fields"] = required_fields
347
+ payload["optional_fields"] = optional_fields
348
+
349
+ for key in ("required_fields", "optional_fields", "runtime_linked_required_fields", "fields", "ambiguous_fields"):
350
+ if key in payload:
351
+ payload[key] = _compact_schema_fields(payload.get(key), template_map=template_map)
352
+
353
+ for key in ("suggested_dimensions", "suggested_metrics", "suggested_time_fields"):
354
+ if isinstance(payload.get(key), list):
355
+ payload[key] = [
356
+ _pick(item, ("field_id", "title")) for item in payload.get(key) if isinstance(item, dict)
357
+ ]
358
+
359
+ for key in ("workflow_node", "view_resolution", "field_count"):
360
+ payload.pop(key, None)
295
361
 
296
362
 
297
363
  def _trim_record_write(payload: JSONObject) -> None:
364
+ payload.pop("verification", None)
298
365
  data = payload.get("data")
299
366
  if not isinstance(data, dict):
300
367
  return
368
+ data.pop("debug", None)
301
369
  data.pop("normalized_payload", None)
302
370
  data.pop("human_review", None)
303
371
  data.pop("action", None)
372
+ resource = _compact_record_resource(data.get("resource"))
373
+ if resource:
374
+ data["resource"] = resource
375
+ else:
376
+ data.pop("resource", None)
377
+ verification = data.get("verification")
378
+ if isinstance(verification, dict):
379
+ compact_verification = _pick(
380
+ verification,
381
+ (
382
+ "verified",
383
+ "verification_mode",
384
+ "field_level_verified",
385
+ ),
386
+ )
387
+ if compact_verification:
388
+ data["verification"] = compact_verification
389
+ else:
390
+ data.pop("verification", None)
391
+ for key in ("blockers", "field_errors", "confirmation_requests", "resolved_fields"):
392
+ value = data.get(key)
393
+ if value in (None, [], {}, ""):
394
+ data.pop(key, None)
304
395
 
305
396
 
306
397
  def _trim_record_get(payload: JSONObject) -> None:
307
- _keep_nested_keys(payload, ("data", "selection", "view"), allowed=("view_id", "name"))
308
- _drop_nested_keys(payload, ("data", "selection"), keys=("columns", "workflow_node_id"))
398
+ data = payload.get("data")
399
+ if not isinstance(data, dict):
400
+ return
401
+ compact: dict[str, Any] = {}
402
+ app_key = data.get("app_key")
403
+ if app_key:
404
+ compact["app_key"] = app_key
405
+ record_id = data.get("record_id")
406
+ if record_id not in (None, ""):
407
+ compact["record_id"] = str(record_id)
408
+ record = data.get("record")
409
+ if isinstance(record, dict):
410
+ compact["record"] = record
411
+ normalized_record = data.get("normalized_record")
412
+ if isinstance(normalized_record, dict):
413
+ compact["normalized_record"] = normalized_record
414
+ normalized_ambiguous_fields = data.get("normalized_ambiguous_fields")
415
+ if isinstance(normalized_ambiguous_fields, dict):
416
+ compact["normalized_ambiguous_fields"] = normalized_ambiguous_fields
417
+ payload["data"] = compact
309
418
 
310
419
 
311
420
  def _trim_record_list(payload: JSONObject) -> None:
312
- _keep_nested_keys(payload, ("data", "selection", "view"), allowed=("view_id", "name"))
313
- _drop_nested_keys(payload, ("data", "selection"), keys=("columns",))
421
+ data = payload.get("data")
422
+ if not isinstance(data, dict):
423
+ return
424
+ pagination = data.get("pagination") if isinstance(data.get("pagination"), dict) else {}
425
+ returned_items = pagination.get("returned_items")
426
+ result_amount = pagination.get("result_amount")
427
+ limit = pagination.get("limit")
428
+ truncated = False
429
+ if isinstance(result_amount, int) and isinstance(returned_items, int):
430
+ truncated = result_amount > returned_items
431
+ compact_pagination = {
432
+ "loaded": True,
433
+ "page_size": limit,
434
+ "fetched_pages": 1,
435
+ "reported_total": result_amount,
436
+ "truncated": truncated,
437
+ }
438
+ selection = data.get("selection") if isinstance(data.get("selection"), dict) else {}
439
+ view = selection.get("view") if isinstance(selection.get("view"), dict) else {}
440
+ compact: dict[str, Any] = {
441
+ "app_key": data.get("app_key"),
442
+ "items": data.get("items") if isinstance(data.get("items"), list) else [],
443
+ "pagination": compact_pagination,
444
+ }
445
+ if view:
446
+ compact["view"] = _pick(view, ("view_id", "name"))
447
+ payload["data"] = compact
314
448
 
315
449
 
316
450
  def _trim_record_analyze(payload: JSONObject) -> None:
317
- _drop_deep_keys(payload, {"debug"})
451
+ summary: dict[str, Any] = {}
452
+ completeness = payload.get("completeness")
453
+ if isinstance(completeness, dict):
454
+ summary["completeness"] = completeness
455
+ presentation = payload.get("presentation")
456
+ if isinstance(presentation, dict):
457
+ summary["presentation"] = presentation
458
+ ranking = payload.get("ranking")
459
+ if isinstance(ranking, dict):
460
+ summary["ranking"] = ranking
461
+ error = payload.get("error")
462
+ if isinstance(error, dict):
463
+ summary["error"] = error
464
+ if summary:
465
+ payload["summary"] = summary
466
+ for key in ("query", "ranking", "ratios", "completeness", "presentation", "error", "debug"):
467
+ payload.pop(key, None)
318
468
 
319
469
 
320
470
  def _trim_code_block_schema(payload: JSONObject) -> None:
@@ -334,6 +484,226 @@ def _trim_task_get(payload: JSONObject) -> None:
334
484
  _drop_deep_keys(payload, {"request_route", "output_profile"})
335
485
 
336
486
 
487
+ def _trim_task_context_detail(payload: JSONObject) -> None:
488
+ _drop_deep_keys(payload, {"request_route", "output_profile"})
489
+
490
+
491
+ def _trim_record_delete(payload: JSONObject) -> None:
492
+ data = payload.get("data")
493
+ if not isinstance(data, dict):
494
+ return
495
+ resource = data.get("resource")
496
+ deleted_ids: list[str] = []
497
+ if isinstance(resource, dict):
498
+ raw_ids = resource.get("record_ids") or resource.get("apply_ids") or resource.get("applyIds")
499
+ if isinstance(raw_ids, list):
500
+ deleted_ids = [str(item) for item in raw_ids if item not in (None, "")]
501
+ data["deleted_ids"] = deleted_ids
502
+ data.setdefault("failed_ids", [])
503
+ for key in (
504
+ "resource",
505
+ "action",
506
+ "normalized_payload",
507
+ "human_review",
508
+ "verification",
509
+ "blockers",
510
+ "field_errors",
511
+ "confirmation_requests",
512
+ "resolved_fields",
513
+ "debug",
514
+ ):
515
+ data.pop(key, None)
516
+
517
+
518
+ def _compact_record_resource(resource: Any) -> dict[str, Any] | None:
519
+ if not isinstance(resource, dict):
520
+ return None
521
+ compact: dict[str, Any] = {}
522
+ if resource.get("type") not in (None, ""):
523
+ compact["type"] = resource.get("type")
524
+ app_key = resource.get("app_key") or resource.get("appKey")
525
+ if app_key not in (None, ""):
526
+ compact["app_key"] = app_key
527
+ record_id = resource.get("record_id")
528
+ if record_id not in (None, ""):
529
+ compact["record_id"] = str(record_id)
530
+ apply_id = resource.get("apply_id") or resource.get("applyId")
531
+ if apply_id not in (None, "") and "record_id" not in compact:
532
+ compact["record_id"] = str(apply_id)
533
+ record_ids = resource.get("record_ids")
534
+ if isinstance(record_ids, list):
535
+ compact["record_ids"] = [str(item) for item in record_ids if item not in (None, "")]
536
+ apply_ids = resource.get("apply_ids") or resource.get("applyIds")
537
+ if isinstance(apply_ids, list) and "record_ids" not in compact:
538
+ compact["record_ids"] = [str(item) for item in apply_ids if item not in (None, "")]
539
+ return compact or None
540
+
541
+
542
+ def _compact_schema_fields(items: Any, *, template_map: dict[str, Any] | None) -> list[JSONObject]:
543
+ if not isinstance(items, list):
544
+ return []
545
+ compacted: list[JSONObject] = []
546
+ for item in items:
547
+ compact = _compact_schema_field(item, template_map=template_map)
548
+ if compact:
549
+ compacted.append(compact)
550
+ return compacted
551
+
552
+
553
+ def _compact_schema_field(item: Any, *, template_map: dict[str, Any] | None) -> JSONObject | None:
554
+ if not isinstance(item, dict):
555
+ return None
556
+ compact: dict[str, Any] = {}
557
+ field_id = item.get("field_id")
558
+ if field_id not in (None, ""):
559
+ compact["field_id"] = field_id
560
+ title = item.get("title")
561
+ if title not in (None, ""):
562
+ compact["title"] = title
563
+ kind = item.get("kind") or item.get("write_kind")
564
+ if kind not in (None, ""):
565
+ compact["kind"] = kind
566
+ if "required" in item:
567
+ compact["required"] = bool(item.get("required"))
568
+ if template_map is not None and isinstance(title, str) and title in template_map:
569
+ compact["template"] = template_map.get(title)
570
+ candidate_hint = item.get("candidate_hint")
571
+ if isinstance(candidate_hint, dict):
572
+ compact["candidate_hint"] = candidate_hint
573
+ options = item.get("options")
574
+ if isinstance(options, list) and options:
575
+ compact["options"] = options
576
+ target_app_key = item.get("target_app_key")
577
+ if isinstance(target_app_key, str) and target_app_key:
578
+ compact["target_app_key"] = target_app_key
579
+ searchable_fields = item.get("searchable_fields")
580
+ if isinstance(searchable_fields, list) and searchable_fields:
581
+ compact["searchable_fields"] = searchable_fields
582
+ row_fields = item.get("row_fields")
583
+ if isinstance(row_fields, list) and row_fields:
584
+ compact["row_fields"] = _compact_schema_fields(row_fields, template_map=None)
585
+ return compact or None
586
+
587
+
588
+ def _compact_import_column(item: dict[str, Any]) -> dict[str, Any]:
589
+ compact: dict[str, Any] = {}
590
+ title = item.get("title")
591
+ if title not in (None, ""):
592
+ compact["title"] = title
593
+ kind = item.get("kind") or item.get("write_kind")
594
+ if kind not in (None, ""):
595
+ compact["kind"] = kind
596
+ compact["required"] = bool(item.get("required"))
597
+ options = item.get("options")
598
+ if isinstance(options, list) and options:
599
+ compact["options"] = options
600
+ if bool(item.get("accepts_natural_input")):
601
+ compact["accepts_natural_input"] = True
602
+ if bool(item.get("requires_upload")):
603
+ compact["requires_upload"] = True
604
+ target_app_key = item.get("target_app_key")
605
+ if isinstance(target_app_key, str) and target_app_key:
606
+ compact["target_app_key"] = target_app_key
607
+ target_app_name = item.get("target_app_name")
608
+ if isinstance(target_app_name, str) and target_app_name:
609
+ compact["target_app_name"] = target_app_name
610
+ searchable_fields = item.get("searchable_fields")
611
+ if isinstance(searchable_fields, list) and searchable_fields:
612
+ compact["searchable_fields"] = searchable_fields
613
+ return compact
614
+
615
+
616
+ def _looks_like_import_verify(payload: JSONObject) -> bool:
617
+ return "verification_id" in payload and "can_import" in payload
618
+
619
+
620
+ def _trim_import_verify_payload(payload: JSONObject) -> None:
621
+ issues = payload.get("issues") if isinstance(payload.get("issues"), list) else []
622
+ issue_summary = _summarize_import_issues(issues)
623
+ payload["issue_summary"] = issue_summary
624
+ columns = payload.get("columns")
625
+ if "expected_columns" not in payload and isinstance(columns, list):
626
+ payload["expected_columns"] = columns
627
+ file_name = payload.get("file_name")
628
+ if not file_name:
629
+ file_path = payload.get("file_path")
630
+ if isinstance(file_path, str) and file_path:
631
+ payload["file_name"] = file_path.split("/")[-1]
632
+ for key in ("apply_rows", "schema_fingerprint", "import_capability", "file_sha256", "verified_file_sha256", "file_format", "local_precheck_limited"):
633
+ payload.pop(key, None)
634
+
635
+
636
+ def _trim_import_repair_payload(payload: JSONObject) -> None:
637
+ payload["verification_id"] = payload.get("new_verification_id") or payload.get("verification_id")
638
+ post_repair_issues = payload.get("post_repair_issues")
639
+ if isinstance(post_repair_issues, list):
640
+ payload["post_repair_issue_summary"] = _summarize_import_issues(post_repair_issues)
641
+ for key in ("new_verification_id", "verification"):
642
+ payload.pop(key, None)
643
+
644
+
645
+ def _trim_import_template_payload(payload: JSONObject) -> None:
646
+ for key in ("schema_fingerprint", "verification"):
647
+ payload.pop(key, None)
648
+
649
+
650
+ def _trim_import_status_payload(payload: JSONObject) -> None:
651
+ total_rows = payload.get("total_rows")
652
+ success_rows = payload.get("success_rows")
653
+ failed_rows = payload.get("failed_rows")
654
+ payload["total"] = total_rows
655
+ if isinstance(success_rows, int) and isinstance(failed_rows, int):
656
+ payload["finished"] = success_rows + failed_rows
657
+ elif isinstance(success_rows, int):
658
+ payload["finished"] = success_rows
659
+ else:
660
+ payload["finished"] = None
661
+ payload["succeeded"] = success_rows
662
+ payload["failed"] = failed_rows
663
+ for key in (
664
+ "matched_by",
665
+ "source_file_name",
666
+ "total_rows",
667
+ "success_rows",
668
+ "failed_rows",
669
+ "error_file_urls",
670
+ "operate_time",
671
+ "operate_user",
672
+ "verification",
673
+ ):
674
+ payload.pop(key, None)
675
+
676
+
677
+ def _summarize_import_issues(issues: list[Any]) -> dict[str, Any]:
678
+ total = 0
679
+ error_count = 0
680
+ warning_count = 0
681
+ sample: list[dict[str, Any]] = []
682
+ for item in issues:
683
+ if not isinstance(item, dict):
684
+ continue
685
+ total += 1
686
+ severity = str(item.get("severity") or "").lower()
687
+ if severity == "error":
688
+ error_count += 1
689
+ if severity == "warning":
690
+ warning_count += 1
691
+ if len(sample) < 3:
692
+ sample.append(
693
+ {
694
+ "code": item.get("code"),
695
+ "message": item.get("message"),
696
+ "severity": item.get("severity"),
697
+ }
698
+ )
699
+ return {
700
+ "total": total,
701
+ "errors": error_count,
702
+ "warnings": warning_count,
703
+ "sample": sample,
704
+ }
705
+
706
+
337
707
  def _trim_directory(payload: JSONObject) -> None:
338
708
  pass
339
709
 
@@ -367,6 +737,7 @@ def _register_policy(domains: tuple[str, ...], names: tuple[str, ...], transform
367
737
  _register_policy((USER_DOMAIN, BUILDER_DOMAIN), ("auth_use_credential", "auth_whoami"), _trim_auth_payload)
368
738
  _register_policy((USER_DOMAIN, BUILDER_DOMAIN), ("auth_logout",), _trim_auth_logout)
369
739
  _register_policy((USER_DOMAIN, BUILDER_DOMAIN), ("workspace_list",), _trim_workspace_list)
740
+ _register_policy((USER_DOMAIN, BUILDER_DOMAIN), ("workspace_get", "workspace_select"), _trim_workspace_get)
370
741
  _register_policy((USER_DOMAIN,), ("app_list", "app_search"), _trim_app_search_like)
371
742
  _register_policy((USER_DOMAIN, BUILDER_DOMAIN), ("app_get",), _trim_app_get)
372
743
  _register_policy((BUILDER_DOMAIN,), ("app_repair_code_blocks",), _trim_builder_list_like)
@@ -402,15 +773,15 @@ _register_policy((USER_DOMAIN,), ("record_list",), _trim_record_list)
402
773
  _register_policy((USER_DOMAIN,), ("record_analyze",), _trim_record_analyze)
403
774
  _register_policy((USER_DOMAIN,), ("record_code_block_run",), _trim_code_block_run)
404
775
  _register_policy((USER_DOMAIN,), ("task_list",), _trim_task_list)
776
+ _register_policy((USER_DOMAIN,), ("task_get",), _trim_task_get)
405
777
  _register_policy(
406
778
  (USER_DOMAIN,),
407
779
  (
408
- "task_get",
409
780
  "task_action_execute",
410
781
  "task_associated_report_detail_get",
411
782
  "task_workflow_log_get",
412
783
  ),
413
- _trim_task_get,
784
+ _trim_task_context_detail,
414
785
  )
415
786
  _register_policy(
416
787
  (USER_DOMAIN,),
@@ -431,10 +802,10 @@ _register_policy(
431
802
  (
432
803
  "record_member_candidates",
433
804
  "record_department_candidates",
434
- "record_delete",
435
805
  ),
436
806
  _trim_builder_list_like,
437
807
  )
808
+ _register_policy((USER_DOMAIN,), ("record_delete",), _trim_record_delete)
438
809
  _register_policy(
439
810
  (BUILDER_DOMAIN,),
440
811
  (
@@ -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`.
@@ -38,7 +38,7 @@ def build_builder_server() -> FastMCP:
38
38
  "If creating or updating an app package may be appropriate, use package_apply with explicit user intent; otherwise use package_get and app_resolve to locate resources, "
39
39
  "app_get/app_get_fields/app_repair_code_blocks/app_get_layout/app_get_views/app_get_flow/app_get_charts/portal_list/portal_get/view_get/chart_get for configuration reads, "
40
40
  "member_search/role_search/role_create when workflow assignees must come from the directory or role catalog, preferring roles over explicit members unless the user explicitly names members, "
41
- "then app_schema_apply/app_layout_apply/app_flow_apply/app_views_apply/app_charts_apply/portal_apply to execute normalized patches; these apply tools perform planning, normalization, and dependency checks internally where applicable. Schema/layout/views noop requests skip publish, charts are immediate-live without publish and resolve targets by chart_id first then exact unique chart name, portal updates are replace-only and publish=false only guarantees draft/base-info updates, and flow should use publish=false whenever you only want draft/precheck behavior. "
41
+ "then app_schema_apply/app_layout_apply/app_flow_apply/app_views_apply/app_charts_apply/portal_apply to execute normalized patches; these apply tools perform planning, normalization, and dependency checks internally where applicable. Schema/layout/views noop requests skip publish, charts are immediate-live without publish and resolve targets by chart_id first then exact unique chart name, portal updates use replace semantics only when sections are supplied and edit-mode base-info-only updates may omit sections, publish=false only guarantees draft/base-info updates, and flow should use publish=false whenever you only want draft/precheck behavior. "
42
42
  "For code_block fields with output bindings, always use qf_output assignment rather than const/let qf_output, and use app_repair_code_blocks when an existing form hangs because output-bound fields stay loading. "
43
43
  "Use package_apply to manage package metadata, visibility, grouping, and ordering, and app_publish_verify for explicit final publish verification. "
44
44
  "For workflow edits, keep the public builder surface on stable linear flows only: start/approve/fill/copy/webhook/end. Branch and condition nodes are intentionally disabled because the backend workflow route is not front-end stable for those node types. Declare node assignees and editable fields explicitly. "
@@ -93,6 +93,16 @@ def build_builder_server() -> FastMCP:
93
93
  include_external=include_external,
94
94
  )
95
95
 
96
+ @server.tool()
97
+ def workspace_get(
98
+ profile: str = DEFAULT_PROFILE,
99
+ ws_id: int | None = None,
100
+ ) -> dict:
101
+ return workspace.workspace_get(
102
+ profile=profile,
103
+ ws_id=ws_id,
104
+ )
105
+
96
106
  @server.tool()
97
107
  def file_upload_local(
98
108
  profile: str = DEFAULT_PROFILE,
@@ -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`.
@@ -228,6 +232,26 @@ If the current MCP capability is unsupported, the workflow is awkward, or the us
228
232
  include_external=include_external,
229
233
  )
230
234
 
235
+ @server.tool()
236
+ def workspace_get(
237
+ profile: str = DEFAULT_PROFILE,
238
+ ws_id: int | None = None,
239
+ ) -> dict:
240
+ return workspace.workspace_get(
241
+ profile=profile,
242
+ ws_id=ws_id,
243
+ )
244
+
245
+ @server.tool()
246
+ def workspace_select(
247
+ profile: str = DEFAULT_PROFILE,
248
+ ws_id: int = 0,
249
+ ) -> dict:
250
+ return workspace.workspace_select(
251
+ profile=profile,
252
+ ws_id=ws_id,
253
+ )
254
+
231
255
  @server.tool()
232
256
  def app_list(profile: str = DEFAULT_PROFILE) -> dict:
233
257
  return apps.app_list(profile=profile)