@josephyan/qingflow-app-builder-mcp 0.2.0-beta.7 → 0.2.0-beta.71

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 (70) hide show
  1. package/README.md +5 -3
  2. package/docs/local-agent-install.md +21 -5
  3. package/npm/bin/qingflow-app-builder-mcp.mjs +1 -1
  4. package/npm/lib/runtime.mjs +168 -12
  5. package/package.json +1 -1
  6. package/pyproject.toml +4 -1
  7. package/skills/qingflow-app-builder/SKILL.md +155 -22
  8. package/skills/qingflow-app-builder/references/create-app.md +51 -21
  9. package/skills/qingflow-app-builder/references/environments.md +1 -1
  10. package/skills/qingflow-app-builder/references/flow-actors-and-permissions.md +123 -0
  11. package/skills/qingflow-app-builder/references/gotchas.md +28 -1
  12. package/skills/qingflow-app-builder/references/solution-playbooks.md +14 -12
  13. package/skills/qingflow-app-builder/references/tool-selection.md +47 -19
  14. package/skills/qingflow-app-builder/references/update-flow.md +112 -25
  15. package/skills/qingflow-app-builder/references/update-layout.md +11 -24
  16. package/skills/qingflow-app-builder/references/update-schema.md +1 -23
  17. package/skills/qingflow-app-builder/references/update-views.md +87 -21
  18. package/skills/qingflow-app-builder-code-integrations/SKILL.md +137 -0
  19. package/skills/qingflow-app-builder-code-integrations/agents/openai.yaml +4 -0
  20. package/skills/qingflow-app-builder-code-integrations/references/code-block.md +66 -0
  21. package/skills/qingflow-app-builder-code-integrations/references/q-linker.md +77 -0
  22. package/src/qingflow_mcp/__init__.py +1 -1
  23. package/src/qingflow_mcp/backend_client.py +210 -0
  24. package/src/qingflow_mcp/builder_facade/models.py +1252 -3
  25. package/src/qingflow_mcp/builder_facade/service.py +11367 -2389
  26. package/src/qingflow_mcp/cli/__init__.py +1 -0
  27. package/src/qingflow_mcp/cli/commands/__init__.py +15 -0
  28. package/src/qingflow_mcp/cli/commands/app.py +40 -0
  29. package/src/qingflow_mcp/cli/commands/auth.py +78 -0
  30. package/src/qingflow_mcp/cli/commands/builder.py +515 -0
  31. package/src/qingflow_mcp/cli/commands/common.py +62 -0
  32. package/src/qingflow_mcp/cli/commands/imports.py +96 -0
  33. package/src/qingflow_mcp/cli/commands/record.py +304 -0
  34. package/src/qingflow_mcp/cli/commands/task.py +89 -0
  35. package/src/qingflow_mcp/cli/commands/workspace.py +33 -0
  36. package/src/qingflow_mcp/cli/context.py +48 -0
  37. package/src/qingflow_mcp/cli/formatters.py +355 -0
  38. package/src/qingflow_mcp/cli/json_io.py +50 -0
  39. package/src/qingflow_mcp/cli/main.py +149 -0
  40. package/src/qingflow_mcp/config.py +39 -0
  41. package/src/qingflow_mcp/import_store.py +121 -0
  42. package/src/qingflow_mcp/list_type_labels.py +24 -0
  43. package/src/qingflow_mcp/response_trim.py +668 -0
  44. package/src/qingflow_mcp/server.py +160 -18
  45. package/src/qingflow_mcp/server_app_builder.py +275 -68
  46. package/src/qingflow_mcp/server_app_user.py +219 -191
  47. package/src/qingflow_mcp/session_store.py +41 -1
  48. package/src/qingflow_mcp/solution/compiler/form_compiler.py +43 -4
  49. package/src/qingflow_mcp/solution/compiler/icon_utils.py +119 -45
  50. package/src/qingflow_mcp/solution/compiler/workflow_compiler.py +41 -2
  51. package/src/qingflow_mcp/solution/executor.py +107 -11
  52. package/src/qingflow_mcp/solution/spec_models.py +2 -0
  53. package/src/qingflow_mcp/tools/ai_builder_tools.py +2032 -127
  54. package/src/qingflow_mcp/tools/app_tools.py +419 -12
  55. package/src/qingflow_mcp/tools/approval_tools.py +571 -72
  56. package/src/qingflow_mcp/tools/auth_tools.py +398 -2
  57. package/src/qingflow_mcp/tools/code_block_tools.py +756 -0
  58. package/src/qingflow_mcp/tools/custom_button_tools.py +179 -0
  59. package/src/qingflow_mcp/tools/directory_tools.py +203 -31
  60. package/src/qingflow_mcp/tools/feedback_tools.py +230 -0
  61. package/src/qingflow_mcp/tools/file_tools.py +1 -0
  62. package/src/qingflow_mcp/tools/import_tools.py +2150 -0
  63. package/src/qingflow_mcp/tools/package_tools.py +18 -4
  64. package/src/qingflow_mcp/tools/portal_tools.py +31 -0
  65. package/src/qingflow_mcp/tools/qingbi_report_tools.py +109 -7
  66. package/src/qingflow_mcp/tools/record_tools.py +9894 -1104
  67. package/src/qingflow_mcp/tools/solution_tools.py +115 -3
  68. package/src/qingflow_mcp/tools/task_context_tools.py +2040 -0
  69. package/src/qingflow_mcp/tools/task_tools.py +376 -225
  70. package/src/qingflow_mcp/tools/workspace_tools.py +163 -19
@@ -1,61 +1,203 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from datetime import date
4
+
3
5
  from mcp.server.fastmcp import FastMCP
4
6
 
5
7
  from .backend_client import BackendClient
6
8
  from .session_store import SessionStore
7
9
  from .tools.app_tools import AppTools
8
10
  from .tools.auth_tools import AuthTools
11
+ from .tools.code_block_tools import CodeBlockTools
12
+ from .tools.feedback_tools import FeedbackTools
9
13
  from .tools.file_tools import FileTools
14
+ from .tools.import_tools import ImportTools
10
15
  from .tools.package_tools import PackageTools
11
16
  from .tools.navigation_tools import NavigationTools
12
- from .tools.approval_tools import ApprovalTools
13
17
  from .tools.directory_tools import DirectoryTools
14
18
  from .tools.portal_tools import PortalTools
15
19
  from .tools.qingbi_report_tools import QingbiReportTools
16
- from .tools.record_tools import RecordTools
17
20
  from .tools.role_tools import RoleTools
18
21
  from .tools.solution_tools import SolutionTools
19
- from .tools.task_tools import TaskTools
22
+ from .tools.task_context_tools import TaskContextTools
20
23
  from .tools.view_tools import ViewTools
21
24
  from .tools.workflow_tools import WorkflowTools
22
25
  from .tools.workspace_tools import WorkspaceTools
23
26
 
24
27
 
25
28
  def build_server() -> FastMCP:
29
+ today = date.today()
30
+ current_year = today.year
26
31
  server = FastMCP(
27
32
  "Qingflow MCP",
28
- instructions=(
29
- "Use auth_login first, then workspace_list and workspace_select. "
30
- "All resource tools operate with the logged-in user's Qingflow permissions.\n\n"
31
- "Task Center (待办/已办) handling:\n"
32
- "- Use task_statistics to get counts of pending tasks (todo_count), timeouts, urged, etc.\n"
33
- "- Use task_list to query tasks. Type values: 1=todo (待办), 2=initiated (我发起的), 3=cc (抄送), 5=done (已办).\n"
34
- "- Use task_list_grouped to get tasks grouped by form/worksheet.\n"
35
- "- Use task_mark_read to mark a specific task as read.\n"
36
- "- Use task_urge to send an urgent reminder for a pending task.\n"
37
- "- Process status values: 1=all, 2=processing, 3=passed, 4=refused, 5=need_supply, 6=urged, 7=timeout, 8=pre_timeout, 9=unread.\n"
38
- "- After identifying the exact task node and record, use record_approve, record_reject, record_rollback, record_transfer, record_reassign, or record_countersign as needed."
39
- ),
33
+ instructions=f"""Use this server for Qingflow operational workflows. Current date: `{today.isoformat()}`.
34
+
35
+ ## Authentication
36
+
37
+ Use `auth_login` first, then `workspace_list` and `workspace_select`.
38
+ All resource tools operate with the logged-in user's Qingflow permissions.
39
+
40
+ ## Shared Helper
41
+
42
+ `feedback_submit` is always available as a cross-cutting helper.
43
+
44
+ - Use it when the current MCP capability is unsupported, awkward, or still cannot satisfy the user's need after reasonable use.
45
+ - It does not require Qingflow login or workspace selection.
46
+ - Call it only after the user explicitly confirms submission.
47
+
48
+ ## App Discovery
49
+
50
+ If `app_key` is unknown, use `app_list` or `app_search` first.
51
+ If the app is known but the data range is not, use `app_get` first and choose from `accessible_views`.
52
+ If an accessible view has `analysis_supported=false`, do not use it for `record_list` or `record_analyze`. `boardView` and `ganttView` are special UI views, not list/analyze targets.
53
+
54
+ ## Schema-First Rule
55
+
56
+ Call `record_insert_schema_get` before `record_insert`.
57
+ Call `record_update_schema_get` before `record_update`.
58
+ Call `record_code_block_schema_get` before `record_code_block_run`.
59
+ Call `app_get` first when the data range is unclear, then use `record_browse_schema_get(view_id=...)` before `record_list`, `record_get`, or `record_analyze`.
60
+ Call `record_import_schema_get` when the import field mapping is unclear before template download or verify.
61
+
62
+ - All `field_id` values must come from the schema response.
63
+ - Never guess field names or ids.
64
+
65
+ ## Schema Scope
66
+
67
+ `record_insert_schema_get` returns the current user's insert-ready applicant schema; read `required_fields`, `optional_fields`, `runtime_linked_required_fields`, and `payload_template`.
68
+ `record_update_schema_get` returns the current record's overall update-ready writable field set across matched accessible views; read `writable_fields` and `payload_template`.
69
+ `record_browse_schema_get(view_id=...)` returns browse-schema fields for the selected accessible view.
70
+ `record_code_block_schema_get` returns code-block-ready schema for exact code block field selection.
71
+ `record_import_schema_get` returns import-ready column metadata.
72
+
73
+ - Hidden fields are omitted.
74
+ - Missing fields mean the field is not visible in the current permission scope.
75
+ - Read the top-level schema payload directly; do not guess missing writable fields.
76
+
77
+ ## Analytics Path
78
+
79
+ `app_get -> record_browse_schema_get(view_id=...) -> record_analyze`
80
+
81
+ Prefer `view_id` entries from `accessible_views` where `analysis_supported=true`.
82
+
83
+ Use this DSL shape:
84
+
85
+ - `dimensions`: `{{field_id, alias, bucket}}`
86
+ - `metrics`: `{{op, field_id, alias}}`
87
+ - `filters`: `{{field_id, op, value}}`
88
+ - `sort`: `{{by, order}}`
89
+
90
+ Important key rules:
91
+
92
+ - Use `op`
93
+ - Do **not** use `type`
94
+ - Do **not** use `agg`
95
+ - Do **not** use `aggregation`
96
+ - Do **not** use `operator`
97
+
98
+ Analysis answers must include concrete numbers. When applicable, include percentages based on the returned totals.
99
+
100
+ ## Record CRUD Path
101
+
102
+ `app_get -> record_browse_schema_get(view_id=...) -> record_list / record_get`
103
+ `record_insert_schema_get -> record_insert`
104
+ `record_update_schema_get -> record_update`
105
+ `record_list / record_get -> record_delete`
106
+ `record_code_block_schema_get -> record_code_block_run`
107
+
108
+ - Use `columns` as `[{{field_id}}]`
109
+ - Use `where` items as `{{field_id, op, value}}`
110
+ - Use `order_by` items as `{{field_id, direction}}`
111
+ - Legacy forms such as bare integer `field_id`, `fieldId`, `operator`, `values`, or `order` may still parse, but they are compatibility-only and not the canonical DSL
112
+
113
+ - `record_insert` uses an applicant-node `fields` map keyed by field title.
114
+ - `record_update` uses a field-title keyed `fields` map and internally selects the first accessible view that can execute the current payload.
115
+ - For insert, `runtime_linked_required_fields` means required-but-not-directly-writable fields that are usually supplied by runtime linkage or upstream context.
116
+ - For insert, fields marked `may_become_required=true` stay in `optional_fields`; they are still directly writable, but linked visibility or option-driven rules can make them required at runtime.
117
+ - Read field-level `linkage` whenever present on `record_insert_schema_get` or `record_update_schema_get`; it is the static hint for linked visibility, reference-driven auto fill, and formula/default auto-fill behavior.
118
+ - `linkage.sources` lists upstream field titles that influence the current field; `linkage.affects_fields` lists downstream fields that may change when the current field changes.
119
+ - `linkage.kind=logic_visibility` means linked visibility or option-driven rules are involved; `linkage.kind=reference_fill` means reference/default matching logic is involved; `linkage.kind=formula_fill` means formula/default auto-fill logic is involved.
120
+ - `record_update_schema_get` exposes the overall writable field set for the record, but not every field combination is guaranteed; `record_update` still needs one single matched accessible view that can cover the payload.
121
+ - `record_delete` deletes by `record_id` or `record_ids`.
122
+ - When readback shape matters after insert or update, prefer `record_get(..., output_profile="normalized")` or `record_list(..., output_profile="normalized")`.
123
+
124
+ - Read relation targets from `record_insert_schema_get` / `record_update_schema_get` relation metadata before preparing relation writes.
125
+ - Member and department fields may be written with natural strings directly on `record_insert` / `record_update`; only fall back to `record_member_candidates` or `record_department_candidates` when the user wants explicit candidate browsing or the write returns ambiguity that needs confirmation.
126
+ - If explicit candidate browsing is needed for default-all member or department fields, prefer those field candidate tools instead of starting with `directory_*`.
127
+
128
+ ## Code Block Path
129
+
130
+ Use `record_code_block_run` when the user wants to execute a form code-block field against an existing record.
131
+
132
+ - Always resolve the exact code-block field from `record_code_block_schema_get` first.
133
+ - Treat code-block execution as write-capable, not read-only.
134
+ - If the code block is bound to relation outputs, Qingflow may calculate target answers and write them back automatically.
135
+ - For safe debugging, pass `apply_writeback=false` and inspect the parsed alias results plus `relation.calculated_answers_preview` before allowing any writeback.
136
+ - In workflow context, pass `role=3` and the exact `workflow_node_id`.
137
+ - After execution, inspect `outputs.configured_aliases`, `outputs.alias_results`, `outputs.alias_map`, `relation.target_fields`, and `writeback.verification` before claiming success.
138
+
139
+ ## Import Path
140
+
141
+ `app_get -> record_import_schema_get -> record_import_template_get -> record_import_verify -> (optional authorized record_import_repair_local) -> record_import_start -> record_import_status_get`
142
+
143
+ - Check `app_get.data.import_capability` before doing import work.
144
+ - If `import_capability.can_import=false`, stop before template download, file repair, or import start.
145
+ - Import must go through `verify -> start`; do not start directly from a raw file path.
146
+ - `record_import_start` requires an explicit `being_enter_auditing` choice. Do not assume a default.
147
+ - Do not modify user-uploaded files unless the user explicitly authorizes repair.
148
+ - If repair is authorized, keep the original file and repair a copy, then run `record_import_verify` again before `record_import_start`.
149
+
150
+ ## Task Workflow Path
151
+
152
+ `task_list -> task_get -> task_action_execute`
153
+
154
+ - Use `task_associated_report_detail_get` for associated view or report details.
155
+ - Use `task_workflow_log_get` for full workflow log history.
156
+ - Task actions operate on `app_key + record_id + workflow_node_id`, not `task_id`.
157
+
158
+ ## Time Handling
159
+
160
+ Normalize relative dates before building DSL.
161
+
162
+ - If the user says `3月` without a year, use the current year: `{current_year}`
163
+ - Convert month-only phrases into explicit legal date ranges
164
+ - Never send impossible dates such as `2026-02-29`
165
+
166
+ ## Environment
167
+
168
+ Default to `prod` unless the user explicitly specifies `test`.
169
+
170
+ ## Constraints
171
+
172
+ Avoid builder-side app or schema changes here.
173
+
174
+ ## Feedback Path
175
+
176
+ If the current MCP capability is unsupported, the workflow is awkward, or the user's need still cannot be satisfied after reasonable use, offer to submit product feedback.
177
+
178
+ - First summarize what is still not working
179
+ - Ask the user whether to submit feedback
180
+ - Call `feedback_submit` only after explicit user confirmation""",
40
181
  )
41
182
  sessions = SessionStore()
42
183
  backend = BackendClient()
43
184
  AuthTools(sessions, backend).register(server)
185
+ FeedbackTools(backend, mcp_side="通用").register(server)
44
186
  WorkspaceTools(sessions, backend).register(server)
45
187
  FileTools(sessions, backend).register(server)
46
- RecordTools(sessions, backend).register(server)
188
+ ImportTools(sessions, backend).register(server)
189
+ CodeBlockTools(sessions, backend).register(server)
190
+ TaskContextTools(sessions, backend).register(server)
47
191
  RoleTools(sessions, backend).register(server)
48
192
  AppTools(sessions, backend).register(server)
49
193
  QingbiReportTools(sessions, backend).register(server)
50
194
  PackageTools(sessions, backend).register(server)
51
195
  NavigationTools(sessions, backend).register(server)
52
- ApprovalTools(sessions, backend).register(server)
53
196
  PortalTools(sessions, backend).register(server)
54
197
  DirectoryTools(sessions, backend).register(server)
55
198
  WorkflowTools(sessions, backend).register(server)
56
199
  ViewTools(sessions, backend).register(server)
57
200
  SolutionTools(sessions, backend).register(server)
58
- TaskTools(sessions, backend).register(server)
59
201
  return server
60
202
 
61
203
 
@@ -1,37 +1,57 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import json
4
+
3
5
  from mcp.server.fastmcp import FastMCP
4
6
 
5
7
  from .backend_client import BackendClient
6
8
  from .config import DEFAULT_PROFILE
9
+ from .response_trim import BUILDER_SERVER_METHOD_MAP, trim_error_response, trim_public_response, wrap_trimmed_methods
7
10
  from .session_store import SessionStore
8
11
  from .tools.ai_builder_tools import AiBuilderTools
9
12
  from .tools.auth_tools import AuthTools
13
+ from .tools.feedback_tools import FeedbackTools
10
14
  from .tools.file_tools import FileTools
11
15
  from .tools.workspace_tools import WorkspaceTools
12
16
 
13
17
 
18
+ def _config_failure(message: str, *, fix_hint: str) -> dict:
19
+ return {
20
+ "status": "failed",
21
+ "error_code": "CONFIG_ERROR",
22
+ "recoverable": True,
23
+ "message": message,
24
+ "details": {"fix_hint": fix_hint},
25
+ "verification": {},
26
+ }
27
+
28
+
14
29
  def build_builder_server() -> FastMCP:
15
30
  server = FastMCP(
16
31
  "Qingflow App Builder MCP",
17
32
  instructions=(
18
33
  "Use this server for AI-native Qingflow builder workflows. "
19
- "Follow the resource path resolve -> summary read -> plan -> apply -> attach -> publish_verify. "
20
- "Use package_resolve/package_list and app_resolve to locate resources, "
21
- "app_read_summary/app_read_fields/app_read_layout_summary/app_read_views_summary/app_read_flow_summary for compact reads, "
22
- "app_schema_plan/app_layout_plan/app_flow_plan/app_views_plan before writes when the target patch is non-trivial, "
23
- "then app_schema_apply/app_layout_apply/app_flow_apply/app_views_apply to execute normalized patches; these apply tools publish by default unless publish=false. "
34
+ "`feedback_submit` is always available as a cross-cutting helper when the current capability is unsupported, awkward, or still cannot satisfy the user's need after reasonable use; it does not require Qingflow login or workspace selection, and it should be called only after explicit user confirmation. "
35
+ "Follow the resource path resolve -> summary read -> apply -> attach -> publish_verify. "
36
+ "Use builder_tool_contract when you need a machine-readable contract, aliases, allowed enums, or a minimal valid example for a public builder tool. "
37
+ "If creating a new package may be appropriate, ask the user to confirm package creation before calling package_create; otherwise use package_resolve/package_list and app_resolve to locate resources, "
38
+ "app_read_summary/app_read_fields/app_read_layout_summary/app_read_views_summary/app_read_flow_summary/app_read_charts_summary/portal_list/portal_get/portal_read_summary/view_get/chart_get for reads, "
39
+ "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, "
40
+ "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. "
24
41
  "Use package_attach_app to attach apps to packages, and app_publish_verify for explicit final publish verification. "
42
+ "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. "
25
43
  "If builder writes are blocked by the current user's own edit lock, use app_release_edit_lock_if_mine with the lock owner details from the failed result. "
26
- "Do not handcraft internal solution payloads or rely on build_id/stage/repair."
44
+ "Do not handcraft internal solution payloads or rely on build_id/stage/repair. "
45
+ "If the current MCP capability is unsupported, the workflow is awkward, or the user's need still cannot be satisfied after reasonable use, first summarize the gap, ask whether to submit feedback, and call feedback_submit only after explicit user confirmation."
27
46
  ),
28
47
  )
29
48
  sessions = SessionStore()
30
49
  backend = BackendClient()
31
- auth = AuthTools(sessions, backend)
32
- workspace = WorkspaceTools(sessions, backend)
33
- files = FileTools(sessions, backend)
34
- ai_builder = AiBuilderTools(sessions, backend)
50
+ auth = wrap_trimmed_methods(AuthTools(sessions, backend), BUILDER_SERVER_METHOD_MAP)
51
+ workspace = wrap_trimmed_methods(WorkspaceTools(sessions, backend), BUILDER_SERVER_METHOD_MAP)
52
+ files = wrap_trimmed_methods(FileTools(sessions, backend), BUILDER_SERVER_METHOD_MAP)
53
+ ai_builder = wrap_trimmed_methods(AiBuilderTools(sessions, backend), BUILDER_SERVER_METHOD_MAP)
54
+ feedback = FeedbackTools(backend, mcp_side="App Builder MCP")
35
55
 
36
56
  @server.tool()
37
57
  def auth_login(
@@ -117,6 +137,46 @@ def build_builder_server() -> FastMCP:
117
137
  file_related_url=file_related_url,
118
138
  )
119
139
 
140
+ @server.tool()
141
+ def feedback_submit(
142
+ category: str = "",
143
+ title: str = "",
144
+ description: str = "",
145
+ expected_behavior: str | None = None,
146
+ actual_behavior: str | None = None,
147
+ impact_scope: str | None = None,
148
+ tool_name: str | None = None,
149
+ app_key: str | None = None,
150
+ record_id: str | int | None = None,
151
+ workflow_node_id: str | int | None = None,
152
+ note: str | None = None,
153
+ ) -> dict:
154
+ try:
155
+ return trim_public_response(
156
+ "feedback_submit",
157
+ feedback.feedback_submit(
158
+ category=category,
159
+ title=title,
160
+ description=description,
161
+ expected_behavior=expected_behavior,
162
+ actual_behavior=actual_behavior,
163
+ impact_scope=impact_scope,
164
+ tool_name=tool_name,
165
+ app_key=app_key,
166
+ record_id=record_id,
167
+ workflow_node_id=workflow_node_id,
168
+ note=note,
169
+ ),
170
+ )
171
+ except RuntimeError as exc:
172
+ try:
173
+ payload = json.loads(str(exc))
174
+ except json.JSONDecodeError:
175
+ raise
176
+ if isinstance(payload, dict):
177
+ raise RuntimeError(json.dumps(trim_error_response(payload), ensure_ascii=False)) from None
178
+ raise
179
+
120
180
  @server.tool()
121
181
  def package_list(profile: str = DEFAULT_PROFILE, trial_status: str = "all") -> dict:
122
182
  return ai_builder.package_list(profile=profile, trial_status=trial_status)
@@ -125,14 +185,69 @@ def build_builder_server() -> FastMCP:
125
185
  def package_resolve(profile: str = DEFAULT_PROFILE, package_name: str = "") -> dict:
126
186
  return ai_builder.package_resolve(profile=profile, package_name=package_name)
127
187
 
188
+ @server.tool()
189
+ def builder_tool_contract(tool_name: str = "") -> dict:
190
+ return ai_builder.builder_tool_contract(tool_name=tool_name)
191
+
192
+ @server.tool()
193
+ def package_create(
194
+ profile: str = DEFAULT_PROFILE,
195
+ package_name: str = "",
196
+ icon: str | None = None,
197
+ color: str | None = None,
198
+ ) -> dict:
199
+ return ai_builder.package_create(profile=profile, package_name=package_name, icon=icon, color=color)
200
+
201
+ @server.tool()
202
+ def member_search(
203
+ profile: str = DEFAULT_PROFILE,
204
+ query: str = "",
205
+ page_num: int = 1,
206
+ page_size: int = 20,
207
+ contain_disable: bool = False,
208
+ ) -> dict:
209
+ return ai_builder.member_search(
210
+ profile=profile,
211
+ query=query,
212
+ page_num=page_num,
213
+ page_size=page_size,
214
+ contain_disable=contain_disable,
215
+ )
216
+
217
+ @server.tool()
218
+ def role_search(
219
+ profile: str = DEFAULT_PROFILE,
220
+ keyword: str = "",
221
+ page_num: int = 1,
222
+ page_size: int = 20,
223
+ ) -> dict:
224
+ return ai_builder.role_search(profile=profile, keyword=keyword, page_num=page_num, page_size=page_size)
225
+
226
+ @server.tool()
227
+ def role_create(
228
+ profile: str = DEFAULT_PROFILE,
229
+ role_name: str = "",
230
+ member_uids: list[int] | None = None,
231
+ member_emails: list[str] | None = None,
232
+ member_names: list[str] | None = None,
233
+ role_icon: str = "ex-user-outlined",
234
+ ) -> dict:
235
+ return ai_builder.role_create(
236
+ profile=profile,
237
+ role_name=role_name,
238
+ member_uids=member_uids or [],
239
+ member_emails=member_emails or [],
240
+ member_names=member_names or [],
241
+ role_icon=role_icon,
242
+ )
243
+
128
244
  @server.tool()
129
245
  def package_attach_app(
130
246
  profile: str = DEFAULT_PROFILE,
131
247
  tag_id: int = 0,
132
248
  app_key: str = "",
133
- app_title: str = "",
134
249
  ) -> dict:
135
- return ai_builder.package_attach_app(profile=profile, tag_id=tag_id, app_key=app_key, app_title=app_title)
250
+ return ai_builder.package_attach_app(profile=profile, tag_id=tag_id, app_key=app_key)
136
251
 
137
252
  @server.tool()
138
253
  def app_release_edit_lock_if_mine(
@@ -155,8 +270,46 @@ def build_builder_server() -> FastMCP:
155
270
  app_name: str = "",
156
271
  package_tag_id: int | None = None,
157
272
  ) -> dict:
273
+ has_app_key = bool((app_key or "").strip())
274
+ has_app_name = bool((app_name or "").strip())
275
+ has_package_tag_id = package_tag_id is not None
276
+ if has_app_key and (has_app_name or has_package_tag_id):
277
+ return _config_failure(
278
+ "app_resolve accepts exactly one selector mode.",
279
+ fix_hint="Use only `app_key`, or use `app_name` together with `package_tag_id`.",
280
+ )
281
+ if not has_app_key and not (has_app_name and has_package_tag_id):
282
+ return _config_failure(
283
+ "app_resolve requires either app_key, or app_name together with package_tag_id.",
284
+ fix_hint="For an existing known app, pass `app_key`. For package-scoped lookup, pass both `app_name` and `package_tag_id`.",
285
+ )
158
286
  return ai_builder.app_resolve(profile=profile, app_key=app_key, app_name=app_name, package_tag_id=package_tag_id)
159
287
 
288
+ @server.tool()
289
+ def app_custom_button_list(profile: str = DEFAULT_PROFILE, app_key: str = "") -> dict:
290
+ return ai_builder.app_custom_button_list(profile=profile, app_key=app_key)
291
+
292
+ @server.tool()
293
+ def app_custom_button_get(profile: str = DEFAULT_PROFILE, app_key: str = "", button_id: int = 0) -> dict:
294
+ return ai_builder.app_custom_button_get(profile=profile, app_key=app_key, button_id=button_id)
295
+
296
+ @server.tool()
297
+ def app_custom_button_create(profile: str = DEFAULT_PROFILE, app_key: str = "", payload: dict | None = None) -> dict:
298
+ return ai_builder.app_custom_button_create(profile=profile, app_key=app_key, payload=payload or {})
299
+
300
+ @server.tool()
301
+ def app_custom_button_update(
302
+ profile: str = DEFAULT_PROFILE,
303
+ app_key: str = "",
304
+ button_id: int = 0,
305
+ payload: dict | None = None,
306
+ ) -> dict:
307
+ return ai_builder.app_custom_button_update(profile=profile, app_key=app_key, button_id=button_id, payload=payload or {})
308
+
309
+ @server.tool()
310
+ def app_custom_button_delete(profile: str = DEFAULT_PROFILE, app_key: str = "", button_id: int = 0) -> dict:
311
+ return ai_builder.app_custom_button_delete(profile=profile, app_key=app_key, button_id=button_id)
312
+
160
313
  @server.tool()
161
314
  def app_read_summary(profile: str = DEFAULT_PROFILE, app_key: str = "") -> dict:
162
315
  return ai_builder.app_read_summary(profile=profile, app_key=app_key)
@@ -178,75 +331,51 @@ def build_builder_server() -> FastMCP:
178
331
  return ai_builder.app_read_flow_summary(profile=profile, app_key=app_key)
179
332
 
180
333
  @server.tool()
181
- def app_schema_plan(
182
- profile: str = DEFAULT_PROFILE,
183
- app_key: str = "",
184
- package_tag_id: int | None = None,
185
- app_name: str = "",
186
- create_if_missing: bool = False,
187
- add_fields: list[dict] | None = None,
188
- update_fields: list[dict] | None = None,
189
- remove_fields: list[dict] | None = None,
190
- ) -> dict:
191
- return ai_builder.app_schema_plan(
192
- profile=profile,
193
- app_key=app_key,
194
- package_tag_id=package_tag_id,
195
- app_name=app_name,
196
- create_if_missing=create_if_missing,
197
- add_fields=add_fields or [],
198
- update_fields=update_fields or [],
199
- remove_fields=remove_fields or [],
200
- )
334
+ def app_read_charts_summary(profile: str = DEFAULT_PROFILE, app_key: str = "") -> dict:
335
+ return ai_builder.app_read_charts_summary(profile=profile, app_key=app_key)
201
336
 
202
337
  @server.tool()
203
- def app_layout_plan(
338
+ def portal_list(profile: str = DEFAULT_PROFILE) -> dict:
339
+ return ai_builder.portal_list(profile=profile)
340
+
341
+ @server.tool()
342
+ def portal_get(
204
343
  profile: str = DEFAULT_PROFILE,
205
- app_key: str = "",
206
- mode: str = "merge",
207
- sections: list[dict] | None = None,
208
- preset: str | None = None,
344
+ dash_key: str = "",
345
+ being_draft: bool = True,
209
346
  ) -> dict:
210
- return ai_builder.app_layout_plan(
211
- profile=profile,
212
- app_key=app_key,
213
- mode=mode,
214
- sections=sections or [],
215
- preset=preset,
216
- )
347
+ return ai_builder.portal_get(profile=profile, dash_key=dash_key, being_draft=being_draft)
217
348
 
218
349
  @server.tool()
219
- def app_flow_plan(
350
+ def portal_read_summary(
220
351
  profile: str = DEFAULT_PROFILE,
221
- app_key: str = "",
222
- mode: str = "replace",
223
- nodes: list[dict] | None = None,
224
- transitions: list[dict] | None = None,
225
- preset: str | None = None,
352
+ dash_key: str = "",
353
+ being_draft: bool = True,
226
354
  ) -> dict:
227
- return ai_builder.app_flow_plan(
228
- profile=profile,
229
- app_key=app_key,
230
- mode=mode,
231
- nodes=nodes or [],
232
- transitions=transitions or [],
233
- preset=preset,
234
- )
355
+ return ai_builder.portal_read_summary(profile=profile, dash_key=dash_key, being_draft=being_draft)
356
+
357
+ @server.tool()
358
+ def view_get(profile: str = DEFAULT_PROFILE, viewgraph_key: str = "") -> dict:
359
+ return ai_builder.view_get(profile=profile, viewgraph_key=viewgraph_key)
235
360
 
236
361
  @server.tool()
237
- def app_views_plan(
362
+ def chart_get(
238
363
  profile: str = DEFAULT_PROFILE,
239
- app_key: str = "",
240
- upsert_views: list[dict] | None = None,
241
- remove_views: list[str] | None = None,
242
- preset: str | None = None,
364
+ chart_id: str = "",
365
+ data_payload: dict | None = None,
366
+ page_num: int | None = None,
367
+ page_size: int | None = None,
368
+ page_num_y: int | None = None,
369
+ page_size_y: int | None = None,
243
370
  ) -> dict:
244
- return ai_builder.app_views_plan(
371
+ return ai_builder.chart_get(
245
372
  profile=profile,
246
- app_key=app_key,
247
- upsert_views=upsert_views or [],
248
- remove_views=remove_views or [],
249
- preset=preset,
373
+ chart_id=chart_id,
374
+ data_payload=data_payload or {},
375
+ page_num=page_num,
376
+ page_size=page_size,
377
+ page_num_y=page_num_y,
378
+ page_size_y=page_size_y,
250
379
  )
251
380
 
252
381
  @server.tool()
@@ -256,18 +385,37 @@ def build_builder_server() -> FastMCP:
256
385
  package_tag_id: int | None = None,
257
386
  app_name: str = "",
258
387
  app_title: str = "",
388
+ icon: str = "",
389
+ color: str = "",
259
390
  create_if_missing: bool = False,
260
391
  publish: bool = True,
261
392
  add_fields: list[dict] | None = None,
262
393
  update_fields: list[dict] | None = None,
263
394
  remove_fields: list[dict] | None = None,
264
395
  ) -> dict:
396
+ has_app_key = bool((app_key or "").strip())
397
+ has_app_name = bool((app_name or "").strip())
398
+ has_app_title = bool((app_title or "").strip())
399
+ has_package_tag_id = package_tag_id is not None
400
+ if has_app_key:
401
+ if create_if_missing or has_app_name or has_package_tag_id or has_app_title:
402
+ return _config_failure(
403
+ "app_schema_apply edit mode only accepts app_key as the resource selector.",
404
+ fix_hint="For existing apps, pass `app_key` only. For create mode, use `package_tag_id + app_name + create_if_missing=true`.",
405
+ )
406
+ elif not (create_if_missing and has_package_tag_id and has_app_name):
407
+ return _config_failure(
408
+ "app_schema_apply create mode requires package_tag_id, app_name, and create_if_missing=true.",
409
+ fix_hint="Use `app_key` for existing apps, or pass `package_tag_id + app_name + create_if_missing=true` to create a new app.",
410
+ )
265
411
  return ai_builder.app_schema_apply(
266
412
  profile=profile,
267
413
  app_key=app_key,
268
414
  package_tag_id=package_tag_id,
269
415
  app_name=app_name,
270
416
  app_title=app_title,
417
+ icon=icon,
418
+ color=color,
271
419
  create_if_missing=create_if_missing,
272
420
  publish=publish,
273
421
  add_fields=add_fields or [],
@@ -319,6 +467,65 @@ def build_builder_server() -> FastMCP:
319
467
  remove_views=remove_views or [],
320
468
  )
321
469
 
470
+ @server.tool()
471
+ def app_charts_apply(
472
+ profile: str = DEFAULT_PROFILE,
473
+ app_key: str = "",
474
+ upsert_charts: list[dict] | None = None,
475
+ remove_chart_ids: list[str] | None = None,
476
+ reorder_chart_ids: list[str] | None = None,
477
+ ) -> dict:
478
+ return ai_builder.app_charts_apply(
479
+ profile=profile,
480
+ app_key=app_key,
481
+ upsert_charts=upsert_charts or [],
482
+ remove_chart_ids=remove_chart_ids or [],
483
+ reorder_chart_ids=reorder_chart_ids or [],
484
+ )
485
+
486
+ @server.tool()
487
+ def portal_apply(
488
+ profile: str = DEFAULT_PROFILE,
489
+ dash_key: str = "",
490
+ dash_name: str = "",
491
+ package_tag_id: int | None = None,
492
+ publish: bool = True,
493
+ sections: list[dict] | None = None,
494
+ auth: dict | None = None,
495
+ icon: str | None = None,
496
+ color: str | None = None,
497
+ hide_copyright: bool | None = None,
498
+ dash_global_config: dict | None = None,
499
+ config: dict | None = None,
500
+ ) -> dict:
501
+ has_dash_key = bool((dash_key or "").strip())
502
+ has_dash_name = bool((dash_name or "").strip())
503
+ has_package_tag_id = package_tag_id is not None
504
+ if has_dash_key and has_package_tag_id:
505
+ return _config_failure(
506
+ "portal_apply accepts exactly one selector mode.",
507
+ fix_hint="Use `dash_key` to update an existing portal, or use `package_tag_id + dash_name` to create a new portal.",
508
+ )
509
+ if not has_dash_key and not (has_package_tag_id and has_dash_name):
510
+ return _config_failure(
511
+ "portal_apply requires either dash_key, or package_tag_id together with dash_name.",
512
+ fix_hint="Use `dash_key` for an existing portal. For create mode, pass `package_tag_id + dash_name`.",
513
+ )
514
+ return ai_builder.portal_apply(
515
+ profile=profile,
516
+ dash_key=dash_key,
517
+ dash_name=dash_name,
518
+ package_tag_id=package_tag_id,
519
+ publish=publish,
520
+ sections=sections or [],
521
+ auth=auth,
522
+ icon=icon,
523
+ color=color,
524
+ hide_copyright=hide_copyright,
525
+ dash_global_config=dash_global_config,
526
+ config=config or {},
527
+ )
528
+
322
529
  @server.tool()
323
530
  def app_publish_verify(
324
531
  profile: str = DEFAULT_PROFILE,