@qingflow-tech/qingflow-app-builder-mcp 1.0.0

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 (105) hide show
  1. package/README.md +32 -0
  2. package/docs/local-agent-install.md +332 -0
  3. package/entry_point.py +13 -0
  4. package/npm/bin/qingflow-app-builder-mcp.mjs +7 -0
  5. package/npm/lib/runtime.mjs +339 -0
  6. package/npm/scripts/postinstall.mjs +16 -0
  7. package/package.json +34 -0
  8. package/pyproject.toml +67 -0
  9. package/qingflow-app-builder-mcp +15 -0
  10. package/skills/qingflow-app-builder/SKILL.md +251 -0
  11. package/skills/qingflow-app-builder/agents/openai.yaml +4 -0
  12. package/skills/qingflow-app-builder/references/create-app.md +128 -0
  13. package/skills/qingflow-app-builder/references/environments.md +63 -0
  14. package/skills/qingflow-app-builder/references/flow-actors-and-permissions.md +123 -0
  15. package/skills/qingflow-app-builder/references/gotchas.md +64 -0
  16. package/skills/qingflow-app-builder/references/solution-playbooks.md +53 -0
  17. package/skills/qingflow-app-builder/references/tool-selection.md +93 -0
  18. package/skills/qingflow-app-builder/references/update-flow.md +158 -0
  19. package/skills/qingflow-app-builder/references/update-layout.md +68 -0
  20. package/skills/qingflow-app-builder/references/update-schema.md +68 -0
  21. package/skills/qingflow-app-builder/references/update-views.md +162 -0
  22. package/skills/qingflow-app-builder-code-integrations/SKILL.md +137 -0
  23. package/skills/qingflow-app-builder-code-integrations/agents/openai.yaml +4 -0
  24. package/skills/qingflow-app-builder-code-integrations/references/code-block.md +66 -0
  25. package/skills/qingflow-app-builder-code-integrations/references/q-linker.md +77 -0
  26. package/src/qingflow_mcp/__init__.py +5 -0
  27. package/src/qingflow_mcp/__main__.py +5 -0
  28. package/src/qingflow_mcp/backend_client.py +649 -0
  29. package/src/qingflow_mcp/builder_facade/__init__.py +3 -0
  30. package/src/qingflow_mcp/builder_facade/models.py +1836 -0
  31. package/src/qingflow_mcp/builder_facade/service.py +15044 -0
  32. package/src/qingflow_mcp/cli/__init__.py +1 -0
  33. package/src/qingflow_mcp/cli/commands/__init__.py +18 -0
  34. package/src/qingflow_mcp/cli/commands/app.py +40 -0
  35. package/src/qingflow_mcp/cli/commands/auth.py +44 -0
  36. package/src/qingflow_mcp/cli/commands/builder.py +538 -0
  37. package/src/qingflow_mcp/cli/commands/chart.py +18 -0
  38. package/src/qingflow_mcp/cli/commands/common.py +62 -0
  39. package/src/qingflow_mcp/cli/commands/imports.py +96 -0
  40. package/src/qingflow_mcp/cli/commands/portal.py +25 -0
  41. package/src/qingflow_mcp/cli/commands/record.py +331 -0
  42. package/src/qingflow_mcp/cli/commands/repo.py +80 -0
  43. package/src/qingflow_mcp/cli/commands/task.py +89 -0
  44. package/src/qingflow_mcp/cli/commands/view.py +18 -0
  45. package/src/qingflow_mcp/cli/commands/workspace.py +25 -0
  46. package/src/qingflow_mcp/cli/context.py +60 -0
  47. package/src/qingflow_mcp/cli/formatters.py +334 -0
  48. package/src/qingflow_mcp/cli/json_io.py +50 -0
  49. package/src/qingflow_mcp/cli/main.py +178 -0
  50. package/src/qingflow_mcp/config.py +513 -0
  51. package/src/qingflow_mcp/errors.py +66 -0
  52. package/src/qingflow_mcp/import_store.py +121 -0
  53. package/src/qingflow_mcp/json_types.py +18 -0
  54. package/src/qingflow_mcp/list_type_labels.py +76 -0
  55. package/src/qingflow_mcp/public_surface.py +233 -0
  56. package/src/qingflow_mcp/repository_store.py +71 -0
  57. package/src/qingflow_mcp/response_trim.py +470 -0
  58. package/src/qingflow_mcp/server.py +212 -0
  59. package/src/qingflow_mcp/server_app_builder.py +533 -0
  60. package/src/qingflow_mcp/server_app_user.py +362 -0
  61. package/src/qingflow_mcp/session_store.py +302 -0
  62. package/src/qingflow_mcp/solution/__init__.py +6 -0
  63. package/src/qingflow_mcp/solution/build_assembly_store.py +181 -0
  64. package/src/qingflow_mcp/solution/compiler/__init__.py +282 -0
  65. package/src/qingflow_mcp/solution/compiler/chart_compiler.py +96 -0
  66. package/src/qingflow_mcp/solution/compiler/form_compiler.py +495 -0
  67. package/src/qingflow_mcp/solution/compiler/icon_utils.py +187 -0
  68. package/src/qingflow_mcp/solution/compiler/navigation_compiler.py +57 -0
  69. package/src/qingflow_mcp/solution/compiler/package_compiler.py +19 -0
  70. package/src/qingflow_mcp/solution/compiler/portal_compiler.py +60 -0
  71. package/src/qingflow_mcp/solution/compiler/view_compiler.py +51 -0
  72. package/src/qingflow_mcp/solution/compiler/workflow_compiler.py +173 -0
  73. package/src/qingflow_mcp/solution/design_session.py +222 -0
  74. package/src/qingflow_mcp/solution/design_store.py +100 -0
  75. package/src/qingflow_mcp/solution/executor.py +2398 -0
  76. package/src/qingflow_mcp/solution/normalizer.py +23 -0
  77. package/src/qingflow_mcp/solution/requirements_builder.py +536 -0
  78. package/src/qingflow_mcp/solution/run_store.py +244 -0
  79. package/src/qingflow_mcp/solution/spec_models.py +855 -0
  80. package/src/qingflow_mcp/tools/__init__.py +1 -0
  81. package/src/qingflow_mcp/tools/ai_builder_tools.py +3419 -0
  82. package/src/qingflow_mcp/tools/app_tools.py +925 -0
  83. package/src/qingflow_mcp/tools/approval_tools.py +1062 -0
  84. package/src/qingflow_mcp/tools/auth_tools.py +875 -0
  85. package/src/qingflow_mcp/tools/base.py +388 -0
  86. package/src/qingflow_mcp/tools/code_block_tools.py +777 -0
  87. package/src/qingflow_mcp/tools/custom_button_tools.py +202 -0
  88. package/src/qingflow_mcp/tools/directory_tools.py +675 -0
  89. package/src/qingflow_mcp/tools/feedback_tools.py +238 -0
  90. package/src/qingflow_mcp/tools/file_tools.py +409 -0
  91. package/src/qingflow_mcp/tools/import_tools.py +2189 -0
  92. package/src/qingflow_mcp/tools/navigation_tools.py +210 -0
  93. package/src/qingflow_mcp/tools/package_tools.py +326 -0
  94. package/src/qingflow_mcp/tools/portal_tools.py +158 -0
  95. package/src/qingflow_mcp/tools/qingbi_report_tools.py +374 -0
  96. package/src/qingflow_mcp/tools/record_tools.py +14037 -0
  97. package/src/qingflow_mcp/tools/repository_dev_tools.py +552 -0
  98. package/src/qingflow_mcp/tools/resource_read_tools.py +421 -0
  99. package/src/qingflow_mcp/tools/role_tools.py +112 -0
  100. package/src/qingflow_mcp/tools/solution_tools.py +4054 -0
  101. package/src/qingflow_mcp/tools/task_context_tools.py +2228 -0
  102. package/src/qingflow_mcp/tools/task_tools.py +890 -0
  103. package/src/qingflow_mcp/tools/view_tools.py +335 -0
  104. package/src/qingflow_mcp/tools/workflow_tools.py +376 -0
  105. package/src/qingflow_mcp/tools/workspace_tools.py +125 -0
@@ -0,0 +1,66 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from dataclasses import asdict, dataclass
5
+
6
+ from .json_types import JSONObject, JSONScalar
7
+
8
+
9
+ INVALID_TOKEN_MARKERS = (
10
+ "invalid token",
11
+ "token invalid",
12
+ "token失效",
13
+ "无效token",
14
+ "登录失效",
15
+ "login token invalid",
16
+ "access token invalid",
17
+ )
18
+
19
+
20
+ @dataclass(slots=True)
21
+ class QingflowApiError(Exception):
22
+ category: str
23
+ message: str
24
+ backend_code: JSONScalar = None
25
+ request_id: str | None = None
26
+ http_status: int | None = None
27
+ details: JSONObject | None = None
28
+
29
+ def to_dict(self) -> JSONObject:
30
+ return asdict(self)
31
+
32
+ def as_json(self) -> str:
33
+ return json.dumps(self.to_dict(), ensure_ascii=False)
34
+
35
+ def __str__(self) -> str:
36
+ return self.as_json()
37
+
38
+ def looks_like_invalid_token(self) -> bool:
39
+ text = self.message.lower()
40
+ return any(marker in text for marker in INVALID_TOKEN_MARKERS)
41
+
42
+ @classmethod
43
+ def auth_required(cls, profile: str) -> "QingflowApiError":
44
+ return cls(
45
+ category="auth",
46
+ message=f"Profile '{profile}' is not logged in. Run auth_use_credential first.",
47
+ )
48
+
49
+ @classmethod
50
+ def workspace_not_selected(cls, profile: str) -> "QingflowApiError":
51
+ return cls(
52
+ category="workspace",
53
+ message=f"WORKSPACE_NOT_SELECTED: profile '{profile}' has no workspace from auth context. Re-run auth_use_credential.",
54
+ )
55
+
56
+ @classmethod
57
+ def config_error(cls, message: str) -> "QingflowApiError":
58
+ return cls(category="config", message=message)
59
+
60
+ @classmethod
61
+ def not_supported(cls, message: str) -> "QingflowApiError":
62
+ return cls(category="not_supported", message=message)
63
+
64
+
65
+ def raise_tool_error(error: QingflowApiError) -> None:
66
+ raise RuntimeError(error.as_json())
@@ -0,0 +1,121 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ from dataclasses import dataclass
6
+ from datetime import datetime, timedelta, timezone
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ from .config import get_mcp_home
11
+
12
+
13
+ def _utc_now() -> datetime:
14
+ return datetime.now(timezone.utc)
15
+
16
+
17
+ def _parse_utc(value: Any) -> datetime | None:
18
+ if not isinstance(value, str) or not value.strip():
19
+ return None
20
+ normalized = value.strip().replace("Z", "+00:00")
21
+ try:
22
+ parsed = datetime.fromisoformat(normalized)
23
+ except ValueError:
24
+ return None
25
+ if parsed.tzinfo is None:
26
+ return parsed.replace(tzinfo=timezone.utc)
27
+ return parsed.astimezone(timezone.utc)
28
+
29
+
30
+ def _json_safe_key(value: str) -> str:
31
+ keep = []
32
+ for char in value:
33
+ if char.isalnum() or char in {"-", "_"}:
34
+ keep.append(char)
35
+ else:
36
+ keep.append("_")
37
+ result = "".join(keep).strip("_")
38
+ return result or "entry"
39
+
40
+
41
+ def _store_dir(env_var: str, default_name: str) -> Path:
42
+ custom = os.getenv(env_var)
43
+ if custom:
44
+ return Path(custom).expanduser()
45
+ return get_mcp_home() / default_name
46
+
47
+
48
+ @dataclass(slots=True)
49
+ class _JsonEntryStore:
50
+ base_dir: Path
51
+ ttl: timedelta
52
+
53
+ def __post_init__(self) -> None:
54
+ self.base_dir.mkdir(parents=True, exist_ok=True)
55
+ self.prune()
56
+
57
+ def put(self, entry_id: str, payload: dict[str, Any]) -> None:
58
+ data = dict(payload)
59
+ data["id"] = entry_id
60
+ data["updated_at"] = _utc_now().isoformat()
61
+ self._path(entry_id).write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
62
+
63
+ def get(self, entry_id: str) -> dict[str, Any] | None:
64
+ path = self._path(entry_id)
65
+ if not path.exists():
66
+ return None
67
+ try:
68
+ payload = json.loads(path.read_text(encoding="utf-8"))
69
+ except (OSError, json.JSONDecodeError):
70
+ path.unlink(missing_ok=True)
71
+ return None
72
+ created_at = _parse_utc(payload.get("created_at")) or _parse_utc(payload.get("updated_at"))
73
+ if created_at is None or _utc_now() - created_at > self.ttl:
74
+ path.unlink(missing_ok=True)
75
+ return None
76
+ return payload
77
+
78
+ def prune(self) -> None:
79
+ for path in self.base_dir.glob("*.json"):
80
+ try:
81
+ payload = json.loads(path.read_text(encoding="utf-8"))
82
+ except (OSError, json.JSONDecodeError):
83
+ path.unlink(missing_ok=True)
84
+ continue
85
+ created_at = _parse_utc(payload.get("created_at")) or _parse_utc(payload.get("updated_at"))
86
+ if created_at is None or _utc_now() - created_at > self.ttl:
87
+ path.unlink(missing_ok=True)
88
+
89
+ def list(self) -> list[dict[str, Any]]:
90
+ entries: list[dict[str, Any]] = []
91
+ self.prune()
92
+ for path in self.base_dir.glob("*.json"):
93
+ try:
94
+ payload = json.loads(path.read_text(encoding="utf-8"))
95
+ except (OSError, json.JSONDecodeError):
96
+ continue
97
+ created_at = _parse_utc(payload.get("created_at")) or _parse_utc(payload.get("updated_at"))
98
+ if created_at is None:
99
+ continue
100
+ entries.append(payload)
101
+ entries.sort(key=lambda item: item.get("created_at") or item.get("updated_at") or "", reverse=True)
102
+ return entries
103
+
104
+ def _path(self, entry_id: str) -> Path:
105
+ return self.base_dir / f"{_json_safe_key(entry_id)}.json"
106
+
107
+
108
+ class ImportVerificationStore(_JsonEntryStore):
109
+ def __init__(self, base_dir: Path | None = None, *, ttl_seconds: int = 3600) -> None:
110
+ super().__init__(
111
+ base_dir=base_dir or _store_dir("QINGFLOW_MCP_IMPORT_VERIFY_HOME", "import-verifications"),
112
+ ttl=timedelta(seconds=ttl_seconds),
113
+ )
114
+
115
+
116
+ class ImportJobStore(_JsonEntryStore):
117
+ def __init__(self, base_dir: Path | None = None, *, ttl_seconds: int = 24 * 3600) -> None:
118
+ super().__init__(
119
+ base_dir=base_dir or _store_dir("QINGFLOW_MCP_IMPORT_JOB_HOME", "import-jobs"),
120
+ ttl=timedelta(seconds=ttl_seconds),
121
+ )
@@ -0,0 +1,18 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Protocol, Any
4
+
5
+ # Use Any for JSON types to avoid Pydantic recursion issues
6
+ # These are used for MCP tool signatures where exact typing is less critical
7
+ JSONScalar = Any
8
+ JSONValue = Any
9
+ JSONObject = dict[str, Any]
10
+ JSONArray = list[Any]
11
+
12
+
13
+ class KeyringBackend(Protocol):
14
+ def set_password(self, service: str, key: str, value: str) -> None: ...
15
+
16
+ def get_password(self, service: str, key: str) -> str | None: ...
17
+
18
+ def delete_password(self, service: str, key: str) -> None: ...
@@ -0,0 +1,76 @@
1
+ from __future__ import annotations
2
+
3
+
4
+ RECORD_LIST_TYPE_LABELS: dict[int, str] = {
5
+ 1: "待办",
6
+ 2: "已办",
7
+ 3: "我发起的-已通过",
8
+ 4: "我发起的-已拒绝",
9
+ 5: "我发起的-草稿",
10
+ 6: "我发起的-待完善",
11
+ 7: "我发起的-流程中",
12
+ 8: "数据管理-全部数据",
13
+ 9: "数据管理-已通过",
14
+ 10: "数据管理-已拒绝",
15
+ 11: "数据管理-流程中",
16
+ 12: "抄送",
17
+ 13: "图表分享者",
18
+ 14: "我发起的",
19
+ 15: "数据管理-已结束",
20
+ 16: "我发起的-已结束",
21
+ }
22
+
23
+ SYSTEM_VIEW_DEFINITIONS: tuple[tuple[str, int, str], ...] = (
24
+ ("system:all", 8, "全部数据"),
25
+ ("system:initiated", 14, "我发起的"),
26
+ ("system:todo", 1, "待办"),
27
+ ("system:done", 2, "已办"),
28
+ ("system:cc", 12, "抄送我的"),
29
+ )
30
+
31
+ SYSTEM_VIEW_ID_TO_LIST_TYPE: dict[str, int] = {view_id: list_type for view_id, list_type, _ in SYSTEM_VIEW_DEFINITIONS}
32
+ SYSTEM_VIEW_ID_TO_NAME: dict[str, str] = {view_id: name for view_id, _, name in SYSTEM_VIEW_DEFINITIONS}
33
+ SYSTEM_LIST_TYPE_TO_VIEW_ID: dict[int, str] = {list_type: view_id for view_id, list_type, _ in SYSTEM_VIEW_DEFINITIONS}
34
+
35
+ TASK_TYPE_LABELS: dict[int, str] = {
36
+ 1: "待办",
37
+ 2: "我发起的",
38
+ 3: "抄送",
39
+ 5: "已办",
40
+ }
41
+
42
+ APP_PUBLISH_STATUS_LABELS: dict[int, str] = {
43
+ 0: "未发布",
44
+ 1: "已发布-有修改",
45
+ 2: "已发布",
46
+ }
47
+
48
+
49
+ def get_record_list_type_label(list_type: int | None) -> str | None:
50
+ if list_type is None:
51
+ return None
52
+ return RECORD_LIST_TYPE_LABELS.get(list_type)
53
+
54
+
55
+ def get_system_view_id(list_type: int | None) -> str | None:
56
+ if list_type is None:
57
+ return None
58
+ return SYSTEM_LIST_TYPE_TO_VIEW_ID.get(list_type)
59
+
60
+
61
+ def get_system_view_name(view_id: str | None) -> str | None:
62
+ if view_id is None:
63
+ return None
64
+ return SYSTEM_VIEW_ID_TO_NAME.get(view_id)
65
+
66
+
67
+ def get_task_type_label(type_value: int | None) -> str | None:
68
+ if type_value is None:
69
+ return None
70
+ return TASK_TYPE_LABELS.get(type_value)
71
+
72
+
73
+ def get_app_publish_status_label(status: int | None) -> str | None:
74
+ if status is None:
75
+ return None
76
+ return APP_PUBLISH_STATUS_LABELS.get(status)
@@ -0,0 +1,233 @@
1
+ from __future__ import annotations
2
+
3
+ from argparse import Namespace
4
+ from dataclasses import dataclass
5
+
6
+
7
+ USER_DOMAIN = "user"
8
+ BUILDER_DOMAIN = "builder"
9
+
10
+
11
+ @dataclass(frozen=True, slots=True)
12
+ class PublicToolSpec:
13
+ domain: str
14
+ tool_name: str
15
+ method_names: tuple[str, ...] = ()
16
+ cli_route: tuple[str, ...] | None = None
17
+ mcp_public: bool = True
18
+ cli_public: bool = True
19
+ has_contract: bool = False
20
+ cli_show_effective_context: bool = False
21
+ cli_context_write: bool = False
22
+
23
+ @property
24
+ def trim_key(self) -> str:
25
+ return tool_key(self.domain, self.tool_name)
26
+
27
+
28
+ def tool_key(domain: str, tool_name: str) -> str:
29
+ return f"{domain}:{tool_name}"
30
+
31
+
32
+ USER_PUBLIC_TOOL_SPECS: tuple[PublicToolSpec, ...] = (
33
+ PublicToolSpec(USER_DOMAIN, "auth_use_credential", ("auth_use_credential",), ("auth", "use-credential")),
34
+ PublicToolSpec(USER_DOMAIN, "auth_whoami", ("auth_whoami",), ("auth", "whoami")),
35
+ PublicToolSpec(USER_DOMAIN, "auth_logout", ("auth_logout",), ("auth", "logout")),
36
+ PublicToolSpec(USER_DOMAIN, "workspace_list", ("workspace_list",), ("workspace", "list")),
37
+ PublicToolSpec(USER_DOMAIN, "app_list", ("app_list",), ("app", "list"), cli_show_effective_context=True),
38
+ PublicToolSpec(USER_DOMAIN, "app_search", ("app_search",), ("app", "search"), cli_show_effective_context=True),
39
+ PublicToolSpec(USER_DOMAIN, "app_get", ("app_get",), ("app", "get"), cli_show_effective_context=True),
40
+ PublicToolSpec(USER_DOMAIN, "portal_list", ("portal_list",), ("portal", "list"), cli_show_effective_context=True),
41
+ PublicToolSpec(USER_DOMAIN, "portal_get", ("portal_get",), ("portal", "get"), cli_show_effective_context=True),
42
+ PublicToolSpec(USER_DOMAIN, "view_get", ("view_get",), ("view", "get"), cli_show_effective_context=True),
43
+ PublicToolSpec(USER_DOMAIN, "chart_get", ("chart_get",), ("chart", "get"), cli_show_effective_context=True),
44
+ PublicToolSpec(USER_DOMAIN, "file_get_upload_info", ("file_get_upload_info",), cli_public=False),
45
+ PublicToolSpec(USER_DOMAIN, "file_upload_local", ("file_upload_local",), cli_public=False),
46
+ PublicToolSpec(USER_DOMAIN, "feedback_submit", ("feedback_submit",), cli_public=False),
47
+ PublicToolSpec(
48
+ USER_DOMAIN,
49
+ "record_schema_get",
50
+ cli_route=("record", "schema", "applicant"),
51
+ mcp_public=False,
52
+ ),
53
+ PublicToolSpec(
54
+ USER_DOMAIN,
55
+ "record_browse_schema_get",
56
+ ("record_browse_schema_get_public",),
57
+ ("record", "schema", "browse"),
58
+ ),
59
+ PublicToolSpec(
60
+ USER_DOMAIN,
61
+ "record_insert_schema_get",
62
+ ("record_insert_schema_get_public",),
63
+ ("record", "schema", "insert"),
64
+ ),
65
+ PublicToolSpec(
66
+ USER_DOMAIN,
67
+ "record_update_schema_get",
68
+ ("record_update_schema_get_public",),
69
+ ("record", "schema", "update"),
70
+ ),
71
+ PublicToolSpec(USER_DOMAIN, "record_import_schema_get", ("record_import_schema_get",), ("record", "schema", "import")),
72
+ PublicToolSpec(
73
+ USER_DOMAIN,
74
+ "record_code_block_schema_get",
75
+ ("record_code_block_schema_get_public",),
76
+ ("record", "schema", "code-block"),
77
+ ),
78
+ PublicToolSpec(USER_DOMAIN, "record_member_candidates", ("record_member_candidates",), cli_public=False),
79
+ PublicToolSpec(USER_DOMAIN, "record_department_candidates", ("record_department_candidates",), cli_public=False),
80
+ PublicToolSpec(USER_DOMAIN, "record_analyze", ("record_analyze",), ("record", "analyze")),
81
+ PublicToolSpec(USER_DOMAIN, "record_list", ("record_list",), ("record", "list"), cli_show_effective_context=True),
82
+ PublicToolSpec(USER_DOMAIN, "record_get", ("record_get_public",), ("record", "get"), cli_show_effective_context=True),
83
+ PublicToolSpec(USER_DOMAIN, "record_insert", ("record_insert_public",), ("record", "insert"), cli_show_effective_context=True, cli_context_write=True),
84
+ PublicToolSpec(USER_DOMAIN, "record_update", ("record_update_public",), ("record", "update"), cli_show_effective_context=True, cli_context_write=True),
85
+ PublicToolSpec(USER_DOMAIN, "record_delete", ("record_delete_public",), ("record", "delete"), cli_show_effective_context=True, cli_context_write=True),
86
+ PublicToolSpec(USER_DOMAIN, "record_import_template_get", ("record_import_template_get",), ("import", "template")),
87
+ PublicToolSpec(USER_DOMAIN, "record_import_verify", ("record_import_verify",), ("import", "verify")),
88
+ PublicToolSpec(USER_DOMAIN, "record_import_repair_local", ("record_import_repair_local",), ("import", "repair")),
89
+ PublicToolSpec(USER_DOMAIN, "record_import_start", ("record_import_start",), ("import", "start")),
90
+ PublicToolSpec(USER_DOMAIN, "record_import_status_get", ("record_import_status_get",), ("import", "status")),
91
+ PublicToolSpec(USER_DOMAIN, "record_code_block_run", ("record_code_block_run",), ("record", "code-block-run"), cli_show_effective_context=True, cli_context_write=True),
92
+ PublicToolSpec(USER_DOMAIN, "task_list", ("task_list",), ("task", "list"), cli_show_effective_context=True),
93
+ PublicToolSpec(USER_DOMAIN, "task_get", ("task_get",), ("task", "get"), cli_show_effective_context=True),
94
+ 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),
96
+ PublicToolSpec(USER_DOMAIN, "task_workflow_log_get", ("task_workflow_log_get",), ("task", "log"), cli_show_effective_context=True),
97
+ PublicToolSpec(USER_DOMAIN, "directory_search", ("directory_search",), cli_public=False),
98
+ PublicToolSpec(USER_DOMAIN, "directory_list_internal_users", ("directory_list_internal_users",), cli_public=False),
99
+ PublicToolSpec(USER_DOMAIN, "directory_list_all_internal_users", ("directory_list_all_internal_users",), cli_public=False),
100
+ PublicToolSpec(USER_DOMAIN, "directory_list_internal_departments", ("directory_list_internal_departments",), cli_public=False),
101
+ PublicToolSpec(USER_DOMAIN, "directory_list_all_departments", ("directory_list_all_departments",), cli_public=False),
102
+ PublicToolSpec(USER_DOMAIN, "directory_list_sub_departments", ("directory_list_sub_departments",), cli_public=False),
103
+ PublicToolSpec(USER_DOMAIN, "directory_list_external_members", ("directory_list_external_members",), cli_public=False),
104
+ )
105
+
106
+
107
+ BUILDER_PUBLIC_TOOL_SPECS: tuple[PublicToolSpec, ...] = (
108
+ PublicToolSpec(BUILDER_DOMAIN, "auth_use_credential", ("auth_use_credential",), ("builder", "auth", "use-credential"), cli_public=False),
109
+ PublicToolSpec(BUILDER_DOMAIN, "auth_whoami", ("auth_whoami",), ("builder", "auth", "whoami"), cli_public=False),
110
+ PublicToolSpec(BUILDER_DOMAIN, "auth_logout", ("auth_logout",), ("builder", "auth", "logout"), cli_public=False),
111
+ PublicToolSpec(BUILDER_DOMAIN, "workspace_list", ("workspace_list",), ("builder", "workspace", "list"), cli_public=False),
112
+ 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
+ PublicToolSpec(BUILDER_DOMAIN, "feedback_submit", ("feedback_submit",), ("builder", "feedback", "submit"), has_contract=True),
114
+ PublicToolSpec(BUILDER_DOMAIN, "builder_tool_contract", ("builder_tool_contract",), ("builder", "contract"), has_contract=False),
115
+ PublicToolSpec(BUILDER_DOMAIN, "package_get", ("package_get",), ("builder", "package", "get"), has_contract=True, cli_show_effective_context=True),
116
+ PublicToolSpec(BUILDER_DOMAIN, "package_apply", ("package_apply",), ("builder", "package", "apply"), has_contract=True, cli_show_effective_context=True, cli_context_write=True),
117
+ PublicToolSpec(BUILDER_DOMAIN, "solution_install", ("solution_install",), ("builder", "solution", "install"), has_contract=True, cli_show_effective_context=True, cli_context_write=True),
118
+ PublicToolSpec(BUILDER_DOMAIN, "member_search", ("member_search",), ("builder", "member", "search"), has_contract=True, cli_show_effective_context=True),
119
+ PublicToolSpec(BUILDER_DOMAIN, "role_search", ("role_search",), ("builder", "role", "search"), has_contract=True, cli_show_effective_context=True),
120
+ PublicToolSpec(BUILDER_DOMAIN, "role_create", ("role_create",), ("builder", "role", "create"), has_contract=True, cli_show_effective_context=True, cli_context_write=True),
121
+ PublicToolSpec(BUILDER_DOMAIN, "app_release_edit_lock_if_mine", ("app_release_edit_lock_if_mine",), ("builder", "app", "release-edit-lock-if-mine"), has_contract=True, cli_show_effective_context=True, cli_context_write=True),
122
+ PublicToolSpec(BUILDER_DOMAIN, "app_resolve", ("app_resolve",), ("builder", "app", "resolve"), has_contract=True, cli_show_effective_context=True),
123
+ PublicToolSpec(BUILDER_DOMAIN, "app_custom_button_list", ("app_custom_button_list",), ("builder", "button", "list"), has_contract=True, cli_show_effective_context=True),
124
+ PublicToolSpec(BUILDER_DOMAIN, "app_custom_button_get", ("app_custom_button_get",), ("builder", "button", "get"), has_contract=True, cli_show_effective_context=True),
125
+ PublicToolSpec(BUILDER_DOMAIN, "app_custom_button_create", ("app_custom_button_create",), ("builder", "button", "create"), has_contract=True, cli_show_effective_context=True, cli_context_write=True),
126
+ PublicToolSpec(BUILDER_DOMAIN, "app_custom_button_update", ("app_custom_button_update",), ("builder", "button", "update"), has_contract=True, cli_show_effective_context=True, cli_context_write=True),
127
+ PublicToolSpec(BUILDER_DOMAIN, "app_custom_button_delete", ("app_custom_button_delete",), ("builder", "button", "delete"), has_contract=True, cli_show_effective_context=True, cli_context_write=True),
128
+ PublicToolSpec(BUILDER_DOMAIN, "app_get", ("app_get",), ("builder", "app", "get", "summary"), has_contract=True, cli_show_effective_context=True),
129
+ PublicToolSpec(BUILDER_DOMAIN, "app_get_fields", ("app_get_fields",), ("builder", "app", "get", "fields"), has_contract=True, cli_show_effective_context=True),
130
+ PublicToolSpec(BUILDER_DOMAIN, "app_repair_code_blocks", ("app_repair_code_blocks",), ("builder", "app", "repair-code-blocks"), has_contract=True, cli_show_effective_context=True),
131
+ PublicToolSpec(BUILDER_DOMAIN, "app_get_layout", ("app_get_layout",), ("builder", "app", "get", "layout"), has_contract=True, cli_show_effective_context=True),
132
+ PublicToolSpec(BUILDER_DOMAIN, "app_get_views", ("app_get_views",), ("builder", "app", "get", "views"), has_contract=True, cli_show_effective_context=True),
133
+ PublicToolSpec(BUILDER_DOMAIN, "app_get_flow", ("app_get_flow",), ("builder", "app", "get", "flow"), has_contract=True, cli_show_effective_context=True),
134
+ PublicToolSpec(BUILDER_DOMAIN, "app_get_charts", ("app_get_charts",), ("builder", "app", "get", "charts"), has_contract=True, cli_show_effective_context=True),
135
+ PublicToolSpec(BUILDER_DOMAIN, "portal_list", ("portal_list",), ("builder", "portal", "list"), has_contract=True, cli_show_effective_context=True),
136
+ PublicToolSpec(BUILDER_DOMAIN, "portal_get", ("portal_get",), ("builder", "portal", "get"), has_contract=True, cli_show_effective_context=True),
137
+ PublicToolSpec(BUILDER_DOMAIN, "view_get", ("view_get",), ("builder", "view", "get"), has_contract=True, cli_show_effective_context=True),
138
+ PublicToolSpec(BUILDER_DOMAIN, "chart_get", ("chart_get",), ("builder", "chart", "get"), has_contract=True, cli_show_effective_context=True),
139
+ PublicToolSpec(BUILDER_DOMAIN, "app_schema_apply", ("app_schema_apply",), ("builder", "schema", "apply"), has_contract=True, cli_show_effective_context=True, cli_context_write=True),
140
+ PublicToolSpec(BUILDER_DOMAIN, "app_layout_apply", ("app_layout_apply",), ("builder", "layout", "apply"), has_contract=True, cli_show_effective_context=True, cli_context_write=True),
141
+ PublicToolSpec(BUILDER_DOMAIN, "app_flow_apply", ("app_flow_apply",), ("builder", "flow", "apply"), has_contract=True, cli_show_effective_context=True, cli_context_write=True),
142
+ PublicToolSpec(BUILDER_DOMAIN, "app_views_apply", ("app_views_apply",), ("builder", "views", "apply"), has_contract=True, cli_show_effective_context=True, cli_context_write=True),
143
+ PublicToolSpec(BUILDER_DOMAIN, "app_charts_apply", ("app_charts_apply",), ("builder", "charts", "apply"), has_contract=True, cli_show_effective_context=True, cli_context_write=True),
144
+ PublicToolSpec(BUILDER_DOMAIN, "portal_apply", ("portal_apply",), ("builder", "portal", "apply"), has_contract=True, cli_show_effective_context=True, cli_context_write=True),
145
+ PublicToolSpec(BUILDER_DOMAIN, "app_publish_verify", ("app_publish_verify",), ("builder", "publish", "verify"), has_contract=True, cli_show_effective_context=True),
146
+ )
147
+
148
+
149
+ ALL_PUBLIC_TOOL_SPECS: tuple[PublicToolSpec, ...] = USER_PUBLIC_TOOL_SPECS + BUILDER_PUBLIC_TOOL_SPECS
150
+ PUBLIC_TOOL_BY_KEY: dict[str, PublicToolSpec] = {spec.trim_key: spec for spec in ALL_PUBLIC_TOOL_SPECS}
151
+ PUBLIC_TOOL_BY_CLI_ROUTE: dict[tuple[str, ...], PublicToolSpec] = {
152
+ spec.cli_route: spec
153
+ for spec in ALL_PUBLIC_TOOL_SPECS
154
+ if spec.cli_public and spec.cli_route is not None
155
+ }
156
+
157
+
158
+ def public_tool_specs(domain: str | None = None) -> tuple[PublicToolSpec, ...]:
159
+ if domain is None:
160
+ return ALL_PUBLIC_TOOL_SPECS
161
+ return tuple(spec for spec in ALL_PUBLIC_TOOL_SPECS if spec.domain == domain)
162
+
163
+
164
+ def public_mcp_tool_names(domain: str) -> set[str]:
165
+ return {spec.tool_name for spec in public_tool_specs(domain) if spec.mcp_public}
166
+
167
+
168
+ def public_builder_contract_tool_names() -> list[str]:
169
+ return sorted(
170
+ spec.tool_name
171
+ for spec in BUILDER_PUBLIC_TOOL_SPECS
172
+ if spec.has_contract and (spec.mcp_public or spec.cli_public)
173
+ )
174
+
175
+
176
+ def server_method_map(domain: str) -> dict[str, str]:
177
+ mapping: dict[str, str] = {}
178
+ for spec in public_tool_specs(domain):
179
+ if not spec.mcp_public:
180
+ continue
181
+ for method_name in spec.method_names:
182
+ mapping[method_name] = spec.trim_key
183
+ return mapping
184
+
185
+
186
+ def cli_trim_key_from_namespace(args: Namespace) -> str | None:
187
+ route = cli_route_from_namespace(args)
188
+ if route is None:
189
+ return None
190
+ spec = PUBLIC_TOOL_BY_CLI_ROUTE.get(route)
191
+ return spec.trim_key if spec is not None else None
192
+
193
+
194
+ def cli_public_tool_spec_from_namespace(args: Namespace) -> PublicToolSpec | None:
195
+ route = cli_route_from_namespace(args)
196
+ if route is None:
197
+ return None
198
+ return PUBLIC_TOOL_BY_CLI_ROUTE.get(route)
199
+
200
+
201
+ def cli_route_from_namespace(args: Namespace) -> tuple[str, ...] | None:
202
+ command = getattr(args, "command", None)
203
+ if not isinstance(command, str) or not command:
204
+ return None
205
+ command = "builder" if command == "build" else command
206
+ if command == "record":
207
+ record_command = getattr(args, "record_command", None)
208
+ if record_command == "schema":
209
+ schema_command = getattr(args, "record_schema_command", None)
210
+ return (command, "schema", schema_command) if isinstance(schema_command, str) and schema_command else None
211
+ return (command, record_command) if isinstance(record_command, str) and record_command else None
212
+ if command == "task":
213
+ task_command = getattr(args, "task_command", None)
214
+ return (command, task_command) if isinstance(task_command, str) and task_command else None
215
+ if command != "builder":
216
+ child_attr = f"{command}_command"
217
+ child = getattr(args, child_attr, None)
218
+ return (command, child) if isinstance(child, str) and child else None
219
+
220
+ builder_command = getattr(args, "builder_command", None)
221
+ if not isinstance(builder_command, str) or not builder_command:
222
+ return None
223
+ if builder_command == "contract":
224
+ return ("builder", "contract")
225
+ if builder_command == "app":
226
+ app_command = getattr(args, "builder_app_command", None)
227
+ if app_command == "get":
228
+ section = getattr(args, "builder_app_get_section", "summary") or "summary"
229
+ return ("builder", "app", "get", str(section))
230
+ return ("builder", "app", app_command) if isinstance(app_command, str) and app_command else None
231
+ child_attr = f"builder_{builder_command.replace('-', '_')}_command"
232
+ child = getattr(args, child_attr, None)
233
+ return ("builder", builder_command, child) if isinstance(child, str) and child else None
@@ -0,0 +1,71 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from dataclasses import dataclass, field
5
+ from datetime import datetime, timezone
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from .config import get_repository_metadata_dir
10
+
11
+
12
+ def _utc_now() -> str:
13
+ return datetime.now(timezone.utc).isoformat()
14
+
15
+
16
+ def _safe_key(value: str) -> str:
17
+ keep: list[str] = []
18
+ for char in value:
19
+ if char.isalnum() or char in {"-", "_"}:
20
+ keep.append(char)
21
+ else:
22
+ keep.append("_")
23
+ normalized = "".join(keep).strip("_")
24
+ return normalized or "repository"
25
+
26
+
27
+ @dataclass(slots=True)
28
+ class RepositoryMetadataStore:
29
+ base_dir: Path | None = None
30
+ _dir: Path = field(init=False, repr=False)
31
+
32
+ def __post_init__(self) -> None:
33
+ self._dir = self.base_dir or get_repository_metadata_dir()
34
+ self._dir.mkdir(parents=True, exist_ok=True)
35
+
36
+ def put(self, repo_name: str, payload: dict[str, Any]) -> dict[str, Any]:
37
+ now = _utc_now()
38
+ existing = self.get(repo_name) or {}
39
+ data = dict(existing)
40
+ data.update(payload)
41
+ data["repo_name"] = repo_name
42
+ data["created_at"] = existing.get("created_at") or now
43
+ data["updated_at"] = now
44
+ self._path(repo_name).write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
45
+ return data
46
+
47
+ def get(self, repo_name: str) -> dict[str, Any] | None:
48
+ path = self._path(repo_name)
49
+ if not path.exists():
50
+ return None
51
+ try:
52
+ payload = json.loads(path.read_text(encoding="utf-8"))
53
+ except (OSError, json.JSONDecodeError):
54
+ path.unlink(missing_ok=True)
55
+ return None
56
+ return payload if isinstance(payload, dict) else None
57
+
58
+ def list(self) -> list[dict[str, Any]]:
59
+ entries: list[dict[str, Any]] = []
60
+ for path in sorted(self._dir.glob("*.json")):
61
+ try:
62
+ payload = json.loads(path.read_text(encoding="utf-8"))
63
+ except (OSError, json.JSONDecodeError):
64
+ continue
65
+ if isinstance(payload, dict):
66
+ entries.append(payload)
67
+ entries.sort(key=lambda item: str(item.get("updated_at") or ""), reverse=True)
68
+ return entries
69
+
70
+ def _path(self, repo_name: str) -> Path:
71
+ return self._dir / f"{_safe_key(repo_name)}.json"