@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.
- package/README.md +2 -2
- package/docs/local-agent-install.md +9 -3
- package/npm/lib/runtime.mjs +10 -3
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/skills/qingflow-app-builder/SKILL.md +88 -184
- package/skills/qingflow-app-builder/references/create-app.md +15 -34
- package/skills/qingflow-app-builder/references/gotchas.md +3 -3
- package/skills/qingflow-app-builder/references/solution-playbooks.md +1 -2
- package/skills/qingflow-app-builder/references/tool-selection.md +9 -10
- package/src/qingflow_mcp/__init__.py +33 -1
- package/src/qingflow_mcp/builder_facade/models.py +14 -4
- package/src/qingflow_mcp/builder_facade/service.py +1582 -124
- package/src/qingflow_mcp/cli/commands/auth.py +69 -1
- package/src/qingflow_mcp/cli/commands/builder.py +4 -3
- package/src/qingflow_mcp/cli/commands/record.py +5 -5
- package/src/qingflow_mcp/cli/commands/task.py +74 -22
- package/src/qingflow_mcp/cli/commands/workspace.py +22 -0
- package/src/qingflow_mcp/cli/formatters.py +287 -48
- package/src/qingflow_mcp/cli/main.py +6 -1
- package/src/qingflow_mcp/cli/qingflow_login.py +116 -0
- package/src/qingflow_mcp/config.py +8 -0
- package/src/qingflow_mcp/errors.py +2 -2
- package/src/qingflow_mcp/id_utils.py +49 -0
- package/src/qingflow_mcp/public_surface.py +11 -1
- package/src/qingflow_mcp/response_trim.py +380 -9
- package/src/qingflow_mcp/server.py +4 -0
- package/src/qingflow_mcp/server_app_builder.py +11 -1
- package/src/qingflow_mcp/server_app_user.py +24 -0
- package/src/qingflow_mcp/session_store.py +69 -15
- package/src/qingflow_mcp/solution/compiler/form_compiler.py +2 -2
- package/src/qingflow_mcp/solution/executor.py +2 -2
- package/src/qingflow_mcp/tools/ai_builder_tools.py +48 -18
- package/src/qingflow_mcp/tools/app_tools.py +1 -0
- package/src/qingflow_mcp/tools/auth_tools.py +271 -12
- package/src/qingflow_mcp/tools/base.py +6 -2
- package/src/qingflow_mcp/tools/code_block_tools.py +2 -2
- package/src/qingflow_mcp/tools/import_tools.py +36 -2
- package/src/qingflow_mcp/tools/record_tools.py +410 -156
- package/src/qingflow_mcp/tools/resource_read_tools.py +114 -32
- package/src/qingflow_mcp/tools/task_context_tools.py +899 -141
- 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(
|
|
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
|
-
|
|
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
|
-
|
|
308
|
-
|
|
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
|
-
|
|
313
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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)
|