@josephyan/qingflow-cli 0.2.0-beta.69 → 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.
@@ -1,9 +1,12 @@
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
@@ -12,6 +15,17 @@ from .tools.file_tools import FileTools
12
15
  from .tools.workspace_tools import WorkspaceTools
13
16
 
14
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
+
15
29
  def build_builder_server() -> FastMCP:
16
30
  server = FastMCP(
17
31
  "Qingflow App Builder MCP",
@@ -21,7 +35,7 @@ def build_builder_server() -> FastMCP:
21
35
  "Follow the resource path resolve -> summary read -> apply -> attach -> publish_verify. "
22
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. "
23
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, "
24
- "app_read_summary/app_read_fields/app_read_layout_summary/app_read_views_summary/app_read_flow_summary/app_read_charts_summary/portal_read_summary for compact reads, "
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, "
25
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, "
26
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. "
27
41
  "Use package_attach_app to attach apps to packages, and app_publish_verify for explicit final publish verification. "
@@ -33,10 +47,10 @@ def build_builder_server() -> FastMCP:
33
47
  )
34
48
  sessions = SessionStore()
35
49
  backend = BackendClient()
36
- auth = AuthTools(sessions, backend)
37
- workspace = WorkspaceTools(sessions, backend)
38
- files = FileTools(sessions, backend)
39
- 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)
40
54
  feedback = FeedbackTools(backend, mcp_side="App Builder MCP")
41
55
 
42
56
  @server.tool()
@@ -123,7 +137,45 @@ def build_builder_server() -> FastMCP:
123
137
  file_related_url=file_related_url,
124
138
  )
125
139
 
126
- feedback.register(server)
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
127
179
 
128
180
  @server.tool()
129
181
  def package_list(profile: str = DEFAULT_PROFILE, trial_status: str = "all") -> dict:
@@ -194,9 +246,8 @@ def build_builder_server() -> FastMCP:
194
246
  profile: str = DEFAULT_PROFILE,
195
247
  tag_id: int = 0,
196
248
  app_key: str = "",
197
- app_title: str = "",
198
249
  ) -> dict:
199
- 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)
200
251
 
201
252
  @server.tool()
202
253
  def app_release_edit_lock_if_mine(
@@ -219,6 +270,19 @@ def build_builder_server() -> FastMCP:
219
270
  app_name: str = "",
220
271
  package_tag_id: int | None = None,
221
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
+ )
222
286
  return ai_builder.app_resolve(profile=profile, app_key=app_key, app_name=app_name, package_tag_id=package_tag_id)
223
287
 
224
288
  @server.tool()
@@ -270,6 +334,18 @@ def build_builder_server() -> FastMCP:
270
334
  def app_read_charts_summary(profile: str = DEFAULT_PROFILE, app_key: str = "") -> dict:
271
335
  return ai_builder.app_read_charts_summary(profile=profile, app_key=app_key)
272
336
 
337
+ @server.tool()
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(
343
+ profile: str = DEFAULT_PROFILE,
344
+ dash_key: str = "",
345
+ being_draft: bool = True,
346
+ ) -> dict:
347
+ return ai_builder.portal_get(profile=profile, dash_key=dash_key, being_draft=being_draft)
348
+
273
349
  @server.tool()
274
350
  def portal_read_summary(
275
351
  profile: str = DEFAULT_PROFILE,
@@ -278,6 +354,30 @@ def build_builder_server() -> FastMCP:
278
354
  ) -> dict:
279
355
  return ai_builder.portal_read_summary(profile=profile, dash_key=dash_key, being_draft=being_draft)
280
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)
360
+
361
+ @server.tool()
362
+ def chart_get(
363
+ profile: str = DEFAULT_PROFILE,
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,
370
+ ) -> dict:
371
+ return ai_builder.chart_get(
372
+ profile=profile,
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,
379
+ )
380
+
281
381
  @server.tool()
282
382
  def app_schema_apply(
283
383
  profile: str = DEFAULT_PROFILE,
@@ -293,6 +393,21 @@ def build_builder_server() -> FastMCP:
293
393
  update_fields: list[dict] | None = None,
294
394
  remove_fields: list[dict] | None = None,
295
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
+ )
296
411
  return ai_builder.app_schema_apply(
297
412
  profile=profile,
298
413
  app_key=app_key,
@@ -383,6 +498,19 @@ def build_builder_server() -> FastMCP:
383
498
  dash_global_config: dict | None = None,
384
499
  config: dict | None = None,
385
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
+ )
386
514
  return ai_builder.portal_apply(
387
515
  profile=profile,
388
516
  dash_key=dash_key,
@@ -1,11 +1,13 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import json
3
4
  from datetime import date
4
5
 
5
6
  from mcp.server.fastmcp import FastMCP
6
7
 
7
8
  from .backend_client import BackendClient
8
9
  from .config import DEFAULT_PROFILE
10
+ from .response_trim import USER_SERVER_METHOD_MAP, trim_error_response, trim_public_response, wrap_trimmed_methods
9
11
  from .session_store import SessionStore
10
12
  from .tools.app_tools import AppTools
11
13
  from .tools.auth_tools import AuthTools
@@ -170,12 +172,15 @@ If the current MCP capability is unsupported, the workflow is awkward, or the us
170
172
  )
171
173
  sessions = SessionStore()
172
174
  backend = BackendClient()
173
- auth = AuthTools(sessions, backend)
174
- apps = AppTools(sessions, backend)
175
- workspace = WorkspaceTools(sessions, backend)
176
- files = FileTools(sessions, backend)
177
- imports = ImportTools(sessions, backend)
175
+ auth = wrap_trimmed_methods(AuthTools(sessions, backend), USER_SERVER_METHOD_MAP)
176
+ apps = wrap_trimmed_methods(AppTools(sessions, backend), USER_SERVER_METHOD_MAP)
177
+ workspace = wrap_trimmed_methods(WorkspaceTools(sessions, backend), USER_SERVER_METHOD_MAP)
178
+ file_tools = wrap_trimmed_methods(FileTools(sessions, backend), USER_SERVER_METHOD_MAP)
179
+ imports = wrap_trimmed_methods(ImportTools(sessions, backend), USER_SERVER_METHOD_MAP)
178
180
  feedback = FeedbackTools(backend, mcp_side="App User MCP")
181
+ code_block_tools = wrap_trimmed_methods(CodeBlockTools(sessions, backend), USER_SERVER_METHOD_MAP)
182
+ task_context_tools = wrap_trimmed_methods(TaskContextTools(sessions, backend), USER_SERVER_METHOD_MAP)
183
+ directory_tools = wrap_trimmed_methods(DirectoryTools(sessions, backend), USER_SERVER_METHOD_MAP)
179
184
 
180
185
  @server.tool()
181
186
  def auth_login(
@@ -263,7 +268,7 @@ If the current MCP capability is unsupported, the workflow is awkward, or the us
263
268
  path_id: int | None = None,
264
269
  file_related_url: str | None = None,
265
270
  ) -> dict:
266
- return files.file_get_upload_info(
271
+ return file_tools.file_get_upload_info(
267
272
  profile=profile,
268
273
  upload_kind=upload_kind,
269
274
  file_name=file_name,
@@ -286,7 +291,7 @@ If the current MCP capability is unsupported, the workflow is awkward, or the us
286
291
  path_id: int | None = None,
287
292
  file_related_url: str | None = None,
288
293
  ) -> dict:
289
- return files.file_upload_local(
294
+ return file_tools.file_upload_local(
290
295
  profile=profile,
291
296
  upload_kind=upload_kind,
292
297
  file_path=file_path,
@@ -299,10 +304,49 @@ If the current MCP capability is unsupported, the workflow is awkward, or the us
299
304
 
300
305
  imports.register(server)
301
306
 
302
- feedback.register(server)
303
- CodeBlockTools(sessions, backend).register(server)
304
- TaskContextTools(sessions, backend).register(server)
305
- DirectoryTools(sessions, backend).register(server)
307
+ @server.tool()
308
+ def feedback_submit(
309
+ category: str = "",
310
+ title: str = "",
311
+ description: str = "",
312
+ expected_behavior: str | None = None,
313
+ actual_behavior: str | None = None,
314
+ impact_scope: str | None = None,
315
+ tool_name: str | None = None,
316
+ app_key: str | None = None,
317
+ record_id: str | int | None = None,
318
+ workflow_node_id: str | int | None = None,
319
+ note: str | None = None,
320
+ ) -> dict:
321
+ try:
322
+ return trim_public_response(
323
+ "feedback_submit",
324
+ feedback.feedback_submit(
325
+ category=category,
326
+ title=title,
327
+ description=description,
328
+ expected_behavior=expected_behavior,
329
+ actual_behavior=actual_behavior,
330
+ impact_scope=impact_scope,
331
+ tool_name=tool_name,
332
+ app_key=app_key,
333
+ record_id=record_id,
334
+ workflow_node_id=workflow_node_id,
335
+ note=note,
336
+ ),
337
+ )
338
+ except RuntimeError as exc:
339
+ try:
340
+ payload = json.loads(str(exc))
341
+ except json.JSONDecodeError:
342
+ raise
343
+ if isinstance(payload, dict):
344
+ raise RuntimeError(json.dumps(trim_error_response(payload), ensure_ascii=False)) from None
345
+ raise
346
+
347
+ code_block_tools.register(server)
348
+ task_context_tools.register(server)
349
+ directory_tools.register(server)
306
350
 
307
351
  return server
308
352