@josephyan/qingflow-cli 0.2.0-beta.1000

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 (92) hide show
  1. package/README.md +31 -0
  2. package/docs/local-agent-install.md +309 -0
  3. package/entry_point.py +13 -0
  4. package/npm/bin/qingflow.mjs +5 -0
  5. package/npm/lib/runtime.mjs +346 -0
  6. package/npm/scripts/postinstall.mjs +16 -0
  7. package/package.json +34 -0
  8. package/pyproject.toml +67 -0
  9. package/qingflow +15 -0
  10. package/src/qingflow_mcp/__init__.py +37 -0
  11. package/src/qingflow_mcp/__main__.py +5 -0
  12. package/src/qingflow_mcp/backend_client.py +649 -0
  13. package/src/qingflow_mcp/builder_facade/__init__.py +3 -0
  14. package/src/qingflow_mcp/builder_facade/models.py +1846 -0
  15. package/src/qingflow_mcp/builder_facade/service.py +16502 -0
  16. package/src/qingflow_mcp/cli/__init__.py +1 -0
  17. package/src/qingflow_mcp/cli/commands/__init__.py +18 -0
  18. package/src/qingflow_mcp/cli/commands/app.py +40 -0
  19. package/src/qingflow_mcp/cli/commands/auth.py +112 -0
  20. package/src/qingflow_mcp/cli/commands/builder.py +539 -0
  21. package/src/qingflow_mcp/cli/commands/chart.py +18 -0
  22. package/src/qingflow_mcp/cli/commands/common.py +62 -0
  23. package/src/qingflow_mcp/cli/commands/imports.py +96 -0
  24. package/src/qingflow_mcp/cli/commands/portal.py +25 -0
  25. package/src/qingflow_mcp/cli/commands/record.py +331 -0
  26. package/src/qingflow_mcp/cli/commands/repo.py +80 -0
  27. package/src/qingflow_mcp/cli/commands/task.py +141 -0
  28. package/src/qingflow_mcp/cli/commands/view.py +18 -0
  29. package/src/qingflow_mcp/cli/commands/workspace.py +110 -0
  30. package/src/qingflow_mcp/cli/context.py +60 -0
  31. package/src/qingflow_mcp/cli/formatters.py +573 -0
  32. package/src/qingflow_mcp/cli/json_io.py +50 -0
  33. package/src/qingflow_mcp/cli/main.py +186 -0
  34. package/src/qingflow_mcp/cli/qingflow_login.py +116 -0
  35. package/src/qingflow_mcp/cli/terminal_ui.py +173 -0
  36. package/src/qingflow_mcp/config.py +407 -0
  37. package/src/qingflow_mcp/errors.py +66 -0
  38. package/src/qingflow_mcp/id_utils.py +49 -0
  39. package/src/qingflow_mcp/import_store.py +121 -0
  40. package/src/qingflow_mcp/json_types.py +18 -0
  41. package/src/qingflow_mcp/list_type_labels.py +76 -0
  42. package/src/qingflow_mcp/public_surface.py +243 -0
  43. package/src/qingflow_mcp/repository_store.py +71 -0
  44. package/src/qingflow_mcp/response_trim.py +841 -0
  45. package/src/qingflow_mcp/server.py +216 -0
  46. package/src/qingflow_mcp/server_app_builder.py +543 -0
  47. package/src/qingflow_mcp/server_app_user.py +386 -0
  48. package/src/qingflow_mcp/session_store.py +369 -0
  49. package/src/qingflow_mcp/solution/__init__.py +6 -0
  50. package/src/qingflow_mcp/solution/build_assembly_store.py +181 -0
  51. package/src/qingflow_mcp/solution/compiler/__init__.py +282 -0
  52. package/src/qingflow_mcp/solution/compiler/chart_compiler.py +96 -0
  53. package/src/qingflow_mcp/solution/compiler/form_compiler.py +495 -0
  54. package/src/qingflow_mcp/solution/compiler/icon_utils.py +187 -0
  55. package/src/qingflow_mcp/solution/compiler/navigation_compiler.py +57 -0
  56. package/src/qingflow_mcp/solution/compiler/package_compiler.py +19 -0
  57. package/src/qingflow_mcp/solution/compiler/portal_compiler.py +60 -0
  58. package/src/qingflow_mcp/solution/compiler/view_compiler.py +51 -0
  59. package/src/qingflow_mcp/solution/compiler/workflow_compiler.py +173 -0
  60. package/src/qingflow_mcp/solution/design_session.py +222 -0
  61. package/src/qingflow_mcp/solution/design_store.py +100 -0
  62. package/src/qingflow_mcp/solution/executor.py +2398 -0
  63. package/src/qingflow_mcp/solution/normalizer.py +23 -0
  64. package/src/qingflow_mcp/solution/requirements_builder.py +536 -0
  65. package/src/qingflow_mcp/solution/run_store.py +244 -0
  66. package/src/qingflow_mcp/solution/spec_models.py +855 -0
  67. package/src/qingflow_mcp/tools/__init__.py +1 -0
  68. package/src/qingflow_mcp/tools/ai_builder_tools.py +3449 -0
  69. package/src/qingflow_mcp/tools/app_tools.py +926 -0
  70. package/src/qingflow_mcp/tools/approval_tools.py +1062 -0
  71. package/src/qingflow_mcp/tools/auth_tools.py +1133 -0
  72. package/src/qingflow_mcp/tools/base.py +281 -0
  73. package/src/qingflow_mcp/tools/code_block_tools.py +777 -0
  74. package/src/qingflow_mcp/tools/custom_button_tools.py +202 -0
  75. package/src/qingflow_mcp/tools/directory_tools.py +675 -0
  76. package/src/qingflow_mcp/tools/feedback_tools.py +238 -0
  77. package/src/qingflow_mcp/tools/file_tools.py +409 -0
  78. package/src/qingflow_mcp/tools/import_tools.py +2223 -0
  79. package/src/qingflow_mcp/tools/navigation_tools.py +210 -0
  80. package/src/qingflow_mcp/tools/package_tools.py +326 -0
  81. package/src/qingflow_mcp/tools/portal_tools.py +158 -0
  82. package/src/qingflow_mcp/tools/qingbi_report_tools.py +374 -0
  83. package/src/qingflow_mcp/tools/record_tools.py +14291 -0
  84. package/src/qingflow_mcp/tools/repository_dev_tools.py +552 -0
  85. package/src/qingflow_mcp/tools/resource_read_tools.py +503 -0
  86. package/src/qingflow_mcp/tools/role_tools.py +112 -0
  87. package/src/qingflow_mcp/tools/solution_tools.py +4054 -0
  88. package/src/qingflow_mcp/tools/task_context_tools.py +2986 -0
  89. package/src/qingflow_mcp/tools/task_tools.py +889 -0
  90. package/src/qingflow_mcp/tools/view_tools.py +335 -0
  91. package/src/qingflow_mcp/tools/workflow_tools.py +376 -0
  92. package/src/qingflow_mcp/tools/workspace_tools.py +266 -0
@@ -0,0 +1,543 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+
5
+ from mcp.server.fastmcp import FastMCP
6
+
7
+ from .backend_client import BackendClient
8
+ from .config import DEFAULT_PROFILE
9
+ from .response_trim import BUILDER_SERVER_METHOD_MAP, trim_error_response, trim_public_response, wrap_trimmed_methods
10
+ from .session_store import SessionStore
11
+ from .tools.ai_builder_tools import AiBuilderTools
12
+ from .tools.auth_tools import AuthTools
13
+ from .tools.feedback_tools import FeedbackTools
14
+ from .tools.file_tools import FileTools
15
+ from .tools.workspace_tools import WorkspaceTools
16
+
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
+
29
+ def build_builder_server() -> FastMCP:
30
+ server = FastMCP(
31
+ "Qingflow App Builder MCP",
32
+ instructions=(
33
+ "Use this server for AI-native Qingflow builder workflows. "
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 -> 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
+ "Use solution_install when the user explicitly wants to install a packaged solution/template by solution_key, optionally copying bundled demo data. "
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
+ "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
+ "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 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
+ "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
+ "Use package_apply to manage package metadata, visibility, grouping, and ordering, and app_publish_verify for explicit final publish verification. "
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. "
45
+ "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. "
46
+ "Do not handcraft internal solution payloads or rely on build_id/stage/repair. "
47
+ "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."
48
+ ),
49
+ )
50
+ sessions = SessionStore()
51
+ backend = BackendClient()
52
+ auth = wrap_trimmed_methods(AuthTools(sessions, backend), BUILDER_SERVER_METHOD_MAP)
53
+ workspace = wrap_trimmed_methods(WorkspaceTools(sessions, backend), BUILDER_SERVER_METHOD_MAP)
54
+ files = wrap_trimmed_methods(FileTools(sessions, backend), BUILDER_SERVER_METHOD_MAP)
55
+ ai_builder = wrap_trimmed_methods(AiBuilderTools(sessions, backend), BUILDER_SERVER_METHOD_MAP)
56
+ feedback = FeedbackTools(backend, mcp_side="App Builder MCP")
57
+
58
+ @server.tool()
59
+ def auth_use_credential(
60
+ profile: str = DEFAULT_PROFILE,
61
+ base_url: str | None = None,
62
+ qf_version: str | None = None,
63
+ credential: str = "",
64
+ persist: bool = False,
65
+ ) -> dict:
66
+ return auth.auth_use_credential(
67
+ profile=profile,
68
+ base_url=base_url,
69
+ qf_version=qf_version,
70
+ credential=credential,
71
+ persist=persist,
72
+ )
73
+
74
+ @server.tool()
75
+ def auth_whoami(profile: str = DEFAULT_PROFILE) -> dict:
76
+ return auth.auth_whoami(profile=profile)
77
+
78
+ @server.tool()
79
+ def auth_logout(profile: str = DEFAULT_PROFILE, forget_persisted: bool = False) -> dict:
80
+ return auth.auth_logout(profile=profile, forget_persisted=forget_persisted)
81
+
82
+ @server.tool()
83
+ def workspace_list(
84
+ profile: str = DEFAULT_PROFILE,
85
+ page_num: int = 1,
86
+ page_size: int = 20,
87
+ include_external: bool = False,
88
+ ) -> dict:
89
+ return workspace.workspace_list(
90
+ profile=profile,
91
+ page_num=page_num,
92
+ page_size=page_size,
93
+ include_external=include_external,
94
+ )
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
+
106
+ @server.tool()
107
+ def file_upload_local(
108
+ profile: str = DEFAULT_PROFILE,
109
+ upload_kind: str = "attachment",
110
+ file_path: str = "",
111
+ upload_mark: str | None = None,
112
+ content_type: str | None = None,
113
+ bucket_type: str | None = None,
114
+ path_id: int | None = None,
115
+ file_related_url: str | None = None,
116
+ ) -> dict:
117
+ return files.file_upload_local(
118
+ profile=profile,
119
+ upload_kind=upload_kind,
120
+ file_path=file_path,
121
+ upload_mark=upload_mark,
122
+ content_type=content_type,
123
+ bucket_type=bucket_type,
124
+ path_id=path_id,
125
+ file_related_url=file_related_url,
126
+ )
127
+
128
+ @server.tool()
129
+ def feedback_submit(
130
+ category: str = "",
131
+ title: str = "",
132
+ description: str = "",
133
+ expected_behavior: str | None = None,
134
+ actual_behavior: str | None = None,
135
+ impact_scope: str | None = None,
136
+ tool_name: str | None = None,
137
+ app_key: str | None = None,
138
+ record_id: str | int | None = None,
139
+ workflow_node_id: str | int | None = None,
140
+ note: str | None = None,
141
+ ) -> dict:
142
+ try:
143
+ return trim_public_response(
144
+ "builder:feedback_submit",
145
+ feedback.feedback_submit(
146
+ category=category,
147
+ title=title,
148
+ description=description,
149
+ expected_behavior=expected_behavior,
150
+ actual_behavior=actual_behavior,
151
+ impact_scope=impact_scope,
152
+ tool_name=tool_name,
153
+ app_key=app_key,
154
+ record_id=record_id,
155
+ workflow_node_id=workflow_node_id,
156
+ note=note,
157
+ ),
158
+ )
159
+ except RuntimeError as exc:
160
+ try:
161
+ payload = json.loads(str(exc))
162
+ except json.JSONDecodeError:
163
+ raise
164
+ if isinstance(payload, dict):
165
+ raise RuntimeError(json.dumps(trim_error_response(payload), ensure_ascii=False)) from None
166
+ raise
167
+
168
+ @server.tool()
169
+ def builder_tool_contract(tool_name: str = "") -> dict:
170
+ return ai_builder.builder_tool_contract(tool_name=tool_name)
171
+
172
+ @server.tool()
173
+ def package_get(profile: str = DEFAULT_PROFILE, package_id: int = 0) -> dict:
174
+ return ai_builder.package_get(profile=profile, package_id=package_id)
175
+
176
+ @server.tool()
177
+ def package_apply(
178
+ profile: str = DEFAULT_PROFILE,
179
+ package_id: int | None = None,
180
+ package_name: str | None = None,
181
+ create_if_missing: bool = False,
182
+ icon: str | None = None,
183
+ color: str | None = None,
184
+ visibility: dict | None = None,
185
+ items: list[dict] | None = None,
186
+ allow_detach: bool = False,
187
+ ) -> dict:
188
+ return ai_builder.package_apply(
189
+ profile=profile,
190
+ package_id=package_id,
191
+ package_name=package_name,
192
+ create_if_missing=create_if_missing,
193
+ icon=icon,
194
+ color=color,
195
+ visibility=visibility,
196
+ items=items,
197
+ allow_detach=allow_detach,
198
+ )
199
+
200
+ @server.tool()
201
+ def solution_install(
202
+ profile: str = DEFAULT_PROFILE,
203
+ solution_key: str = "",
204
+ being_copy_data: bool = True,
205
+ solution_source: str = "solutionDetail",
206
+ ) -> dict:
207
+ return ai_builder.solution_install(
208
+ profile=profile,
209
+ solution_key=solution_key,
210
+ being_copy_data=being_copy_data,
211
+ solution_source=solution_source,
212
+ )
213
+
214
+ @server.tool()
215
+ def member_search(
216
+ profile: str = DEFAULT_PROFILE,
217
+ query: str = "",
218
+ page_num: int = 1,
219
+ page_size: int = 20,
220
+ contain_disable: bool = False,
221
+ ) -> dict:
222
+ return ai_builder.member_search(
223
+ profile=profile,
224
+ query=query,
225
+ page_num=page_num,
226
+ page_size=page_size,
227
+ contain_disable=contain_disable,
228
+ )
229
+
230
+ @server.tool()
231
+ def role_search(
232
+ profile: str = DEFAULT_PROFILE,
233
+ keyword: str = "",
234
+ page_num: int = 1,
235
+ page_size: int = 20,
236
+ ) -> dict:
237
+ return ai_builder.role_search(profile=profile, keyword=keyword, page_num=page_num, page_size=page_size)
238
+
239
+ @server.tool()
240
+ def role_create(
241
+ profile: str = DEFAULT_PROFILE,
242
+ role_name: str = "",
243
+ member_uids: list[int] | None = None,
244
+ member_emails: list[str] | None = None,
245
+ member_names: list[str] | None = None,
246
+ role_icon: str = "ex-user-outlined",
247
+ ) -> dict:
248
+ return ai_builder.role_create(
249
+ profile=profile,
250
+ role_name=role_name,
251
+ member_uids=member_uids or [],
252
+ member_emails=member_emails or [],
253
+ member_names=member_names or [],
254
+ role_icon=role_icon,
255
+ )
256
+
257
+ @server.tool()
258
+ def app_release_edit_lock_if_mine(
259
+ profile: str = DEFAULT_PROFILE,
260
+ app_key: str = "",
261
+ lock_owner_email: str = "",
262
+ lock_owner_name: str = "",
263
+ ) -> dict:
264
+ return ai_builder.app_release_edit_lock_if_mine(
265
+ profile=profile,
266
+ app_key=app_key,
267
+ lock_owner_email=lock_owner_email,
268
+ lock_owner_name=lock_owner_name,
269
+ )
270
+
271
+ @server.tool()
272
+ def app_resolve(
273
+ profile: str = DEFAULT_PROFILE,
274
+ app_key: str = "",
275
+ app_name: str = "",
276
+ package_id: int | None = None,
277
+ ) -> dict:
278
+ has_app_key = bool((app_key or "").strip())
279
+ has_app_name = bool((app_name or "").strip())
280
+ has_package_id = package_id is not None
281
+ if has_app_key and (has_app_name or has_package_id):
282
+ return _config_failure(
283
+ "app_resolve accepts exactly one selector mode.",
284
+ fix_hint="Use only `app_key`, or use `app_name` together with `package_id`.",
285
+ )
286
+ if not has_app_key and not (has_app_name and has_package_id):
287
+ return _config_failure(
288
+ "app_resolve requires either app_key, or app_name together with package_id.",
289
+ fix_hint="For an existing known app, pass `app_key`. For package-scoped lookup, pass both `app_name` and `package_id`.",
290
+ )
291
+ return ai_builder.app_resolve(profile=profile, app_key=app_key, app_name=app_name, package_id=package_id)
292
+
293
+ @server.tool()
294
+ def app_custom_button_list(profile: str = DEFAULT_PROFILE, app_key: str = "") -> dict:
295
+ return ai_builder.app_custom_button_list(profile=profile, app_key=app_key)
296
+
297
+ @server.tool()
298
+ def app_custom_button_get(profile: str = DEFAULT_PROFILE, app_key: str = "", button_id: int = 0) -> dict:
299
+ return ai_builder.app_custom_button_get(profile=profile, app_key=app_key, button_id=button_id)
300
+
301
+ @server.tool()
302
+ def app_custom_button_create(profile: str = DEFAULT_PROFILE, app_key: str = "", payload: dict | None = None) -> dict:
303
+ return ai_builder.app_custom_button_create(profile=profile, app_key=app_key, payload=payload or {})
304
+
305
+ @server.tool()
306
+ def app_custom_button_update(
307
+ profile: str = DEFAULT_PROFILE,
308
+ app_key: str = "",
309
+ button_id: int = 0,
310
+ payload: dict | None = None,
311
+ ) -> dict:
312
+ return ai_builder.app_custom_button_update(profile=profile, app_key=app_key, button_id=button_id, payload=payload or {})
313
+
314
+ @server.tool()
315
+ def app_custom_button_delete(profile: str = DEFAULT_PROFILE, app_key: str = "", button_id: int = 0) -> dict:
316
+ return ai_builder.app_custom_button_delete(profile=profile, app_key=app_key, button_id=button_id)
317
+
318
+ @server.tool()
319
+ def app_get(profile: str = DEFAULT_PROFILE, app_key: str = "") -> dict:
320
+ return ai_builder.app_get(profile=profile, app_key=app_key)
321
+
322
+ @server.tool()
323
+ def app_get_fields(profile: str = DEFAULT_PROFILE, app_key: str = "") -> dict:
324
+ return ai_builder.app_get_fields(profile=profile, app_key=app_key)
325
+
326
+ @server.tool()
327
+ def app_repair_code_blocks(
328
+ profile: str = DEFAULT_PROFILE,
329
+ app_key: str = "",
330
+ field: str | None = None,
331
+ apply: bool = False,
332
+ ) -> dict:
333
+ return ai_builder.app_repair_code_blocks(profile=profile, app_key=app_key, field=field, apply=apply)
334
+
335
+ @server.tool()
336
+ def app_get_layout(profile: str = DEFAULT_PROFILE, app_key: str = "") -> dict:
337
+ return ai_builder.app_get_layout(profile=profile, app_key=app_key)
338
+
339
+ @server.tool()
340
+ def app_get_views(profile: str = DEFAULT_PROFILE, app_key: str = "") -> dict:
341
+ return ai_builder.app_get_views(profile=profile, app_key=app_key)
342
+
343
+ @server.tool()
344
+ def app_get_flow(profile: str = DEFAULT_PROFILE, app_key: str = "") -> dict:
345
+ return ai_builder.app_get_flow(profile=profile, app_key=app_key)
346
+
347
+ @server.tool()
348
+ def app_get_charts(profile: str = DEFAULT_PROFILE, app_key: str = "") -> dict:
349
+ return ai_builder.app_get_charts(profile=profile, app_key=app_key)
350
+
351
+ @server.tool()
352
+ def portal_list(profile: str = DEFAULT_PROFILE) -> dict:
353
+ return ai_builder.portal_list(profile=profile)
354
+
355
+ @server.tool()
356
+ def portal_get(
357
+ profile: str = DEFAULT_PROFILE,
358
+ dash_key: str = "",
359
+ being_draft: bool = True,
360
+ ) -> dict:
361
+ return ai_builder.portal_get(profile=profile, dash_key=dash_key, being_draft=being_draft)
362
+
363
+ @server.tool()
364
+ def view_get(profile: str = DEFAULT_PROFILE, view_key: str = "") -> dict:
365
+ return ai_builder.view_get(profile=profile, view_key=view_key)
366
+
367
+ @server.tool()
368
+ def chart_get(profile: str = DEFAULT_PROFILE, chart_id: str = "") -> dict:
369
+ return ai_builder.chart_get(profile=profile, chart_id=chart_id)
370
+
371
+ @server.tool()
372
+ def app_schema_apply(
373
+ profile: str = DEFAULT_PROFILE,
374
+ app_key: str = "",
375
+ package_id: int | None = None,
376
+ app_name: str = "",
377
+ app_title: str = "",
378
+ icon: str = "",
379
+ color: str = "",
380
+ visibility: dict | None = None,
381
+ create_if_missing: bool = False,
382
+ publish: bool = True,
383
+ add_fields: list[dict] | None = None,
384
+ update_fields: list[dict] | None = None,
385
+ remove_fields: list[dict] | None = None,
386
+ ) -> dict:
387
+ has_app_key = bool((app_key or "").strip())
388
+ has_app_name = bool((app_name or "").strip())
389
+ has_app_title = bool((app_title or "").strip())
390
+ has_package_id = package_id is not None
391
+ if has_app_key:
392
+ if create_if_missing or has_package_id:
393
+ return _config_failure(
394
+ "app_schema_apply edit mode accepts app_key and optional app_name rename only.",
395
+ fix_hint="For existing apps, use `app_key` and optionally `app_name`. For create mode, use `package_id + app_name + create_if_missing=true`.",
396
+ )
397
+ elif not (create_if_missing and has_package_id and (has_app_name or has_app_title)):
398
+ return _config_failure(
399
+ "app_schema_apply create mode requires package_id, app_name, and create_if_missing=true.",
400
+ fix_hint="Use `app_key` for existing apps, or pass `package_id + app_name + create_if_missing=true` to create a new app.",
401
+ )
402
+ return ai_builder.app_schema_apply(
403
+ profile=profile,
404
+ app_key=app_key,
405
+ package_id=package_id,
406
+ app_name=app_name,
407
+ app_title=app_title,
408
+ icon=icon,
409
+ color=color,
410
+ visibility=visibility,
411
+ create_if_missing=create_if_missing,
412
+ publish=publish,
413
+ add_fields=add_fields or [],
414
+ update_fields=update_fields or [],
415
+ remove_fields=remove_fields or [],
416
+ )
417
+
418
+ @server.tool()
419
+ def app_layout_apply(
420
+ profile: str = DEFAULT_PROFILE,
421
+ app_key: str = "",
422
+ mode: str = "merge",
423
+ publish: bool = True,
424
+ sections: list[dict] | None = None,
425
+ ) -> dict:
426
+ return ai_builder.app_layout_apply(profile=profile, app_key=app_key, mode=mode, publish=publish, sections=sections or [])
427
+
428
+ @server.tool()
429
+ def app_flow_apply(
430
+ profile: str = DEFAULT_PROFILE,
431
+ app_key: str = "",
432
+ mode: str = "replace",
433
+ publish: bool = True,
434
+ nodes: list[dict] | None = None,
435
+ transitions: list[dict] | None = None,
436
+ ) -> dict:
437
+ return ai_builder.app_flow_apply(
438
+ profile=profile,
439
+ app_key=app_key,
440
+ mode=mode,
441
+ publish=publish,
442
+ nodes=nodes or [],
443
+ transitions=transitions or [],
444
+ )
445
+
446
+ @server.tool()
447
+ def app_views_apply(
448
+ profile: str = DEFAULT_PROFILE,
449
+ app_key: str = "",
450
+ publish: bool = True,
451
+ upsert_views: list[dict] | None = None,
452
+ remove_views: list[str] | None = None,
453
+ ) -> dict:
454
+ return ai_builder.app_views_apply(
455
+ profile=profile,
456
+ app_key=app_key,
457
+ publish=publish,
458
+ upsert_views=upsert_views or [],
459
+ remove_views=remove_views or [],
460
+ )
461
+
462
+ @server.tool()
463
+ def app_charts_apply(
464
+ profile: str = DEFAULT_PROFILE,
465
+ app_key: str = "",
466
+ upsert_charts: list[dict] | None = None,
467
+ remove_chart_ids: list[str] | None = None,
468
+ reorder_chart_ids: list[str] | None = None,
469
+ ) -> dict:
470
+ return ai_builder.app_charts_apply(
471
+ profile=profile,
472
+ app_key=app_key,
473
+ upsert_charts=upsert_charts or [],
474
+ remove_chart_ids=remove_chart_ids or [],
475
+ reorder_chart_ids=reorder_chart_ids or [],
476
+ )
477
+
478
+ @server.tool()
479
+ def portal_apply(
480
+ profile: str = DEFAULT_PROFILE,
481
+ dash_key: str = "",
482
+ dash_name: str = "",
483
+ package_id: int | None = None,
484
+ publish: bool = True,
485
+ sections: list[dict] | None = None,
486
+ visibility: dict | None = None,
487
+ auth: dict | None = None,
488
+ icon: str | None = None,
489
+ color: str | None = None,
490
+ hide_copyright: bool | None = None,
491
+ dash_global_config: dict | None = None,
492
+ config: dict | None = None,
493
+ ) -> dict:
494
+ has_dash_key = bool((dash_key or "").strip())
495
+ has_dash_name = bool((dash_name or "").strip())
496
+ has_package_id = package_id is not None
497
+ if has_dash_key and has_package_id:
498
+ return _config_failure(
499
+ "portal_apply accepts exactly one selector mode.",
500
+ fix_hint="Use `dash_key` to update an existing portal, or use `package_id + dash_name` to create a new portal.",
501
+ )
502
+ if not has_dash_key and not (has_package_id and has_dash_name):
503
+ return _config_failure(
504
+ "portal_apply requires either dash_key, or package_id together with dash_name.",
505
+ fix_hint="Use `dash_key` for an existing portal. For create mode, pass `package_id + dash_name`.",
506
+ )
507
+ return ai_builder.portal_apply(
508
+ profile=profile,
509
+ dash_key=dash_key,
510
+ dash_name=dash_name,
511
+ package_id=package_id,
512
+ publish=publish,
513
+ sections=sections or [],
514
+ visibility=visibility,
515
+ auth=auth,
516
+ icon=icon,
517
+ color=color,
518
+ hide_copyright=hide_copyright,
519
+ dash_global_config=dash_global_config,
520
+ config=config or {},
521
+ )
522
+
523
+ @server.tool()
524
+ def app_publish_verify(
525
+ profile: str = DEFAULT_PROFILE,
526
+ app_key: str = "",
527
+ expected_package_id: int | None = None,
528
+ ) -> dict:
529
+ return ai_builder.app_publish_verify(
530
+ profile=profile,
531
+ app_key=app_key,
532
+ expected_package_id=expected_package_id,
533
+ )
534
+
535
+ return server
536
+
537
+
538
+ def main() -> None:
539
+ build_builder_server().run()
540
+
541
+
542
+ if __name__ == "__main__":
543
+ main()