@josephyan/qingflow-app-builder-mcp 1.1.25 → 1.1.26

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 (98) hide show
  1. package/README.md +5 -6
  2. package/docs/local-agent-install.md +6 -57
  3. package/entry_point.py +1 -1
  4. package/npm/bin/qingflow-app-builder-mcp.mjs +33 -2
  5. package/npm/lib/runtime.mjs +101 -21
  6. package/npm/scripts/postinstall.mjs +10 -1
  7. package/package.json +2 -3
  8. package/pyproject.toml +1 -1
  9. package/skills/qingflow-app-builder/SKILL.md +42 -104
  10. package/skills/qingflow-app-builder/references/build-complete-system.md +428 -0
  11. package/skills/qingflow-app-builder/references/build-single-app.md +530 -0
  12. package/skills/qingflow-app-builder/references/create-app.md +11 -30
  13. package/skills/qingflow-app-builder/references/environments.md +1 -1
  14. package/skills/qingflow-app-builder/references/gotchas.md +9 -31
  15. package/skills/qingflow-app-builder/references/match-rules.md +0 -15
  16. package/skills/qingflow-app-builder/references/solution-playbooks.md +0 -10
  17. package/skills/qingflow-app-builder/references/tool-selection.md +23 -38
  18. package/skills/qingflow-app-builder/references/update-flow.md +38 -22
  19. package/skills/qingflow-app-builder/references/update-schema.md +0 -14
  20. package/skills/qingflow-app-builder/references/update-views.md +14 -83
  21. package/skills/qingflow-app-builder-code-integrations/SKILL.md +3 -5
  22. package/skills/qingflow-app-builder-code-integrations/references/code-block.md +1 -1
  23. package/skills/qingflow-app-builder-code-integrations/references/q-linker.md +1 -1
  24. package/skills/qingflow-mcp-setup/SKILL.md +111 -0
  25. package/skills/qingflow-mcp-setup/agents/openai.yaml +4 -0
  26. package/skills/qingflow-mcp-setup/references/claude-desktop.md +34 -0
  27. package/skills/qingflow-mcp-setup/references/environments.md +61 -0
  28. package/skills/qingflow-mcp-setup/references/generic-stdio.md +31 -0
  29. package/skills/qingflow-mcp-setup/scripts/check_local_server.sh +38 -0
  30. package/skills/qingflow-workflow-builder/SKILL.md +96 -0
  31. package/skills/qingflow-workflow-builder/manifest.yaml +8 -0
  32. package/skills/qingflow-workflow-builder/references/01-overview.md +45 -0
  33. package/skills/qingflow-workflow-builder/references/02-update-mode.md +53 -0
  34. package/skills/qingflow-workflow-builder/references/03-flow-patterns.md +57 -0
  35. package/skills/qingflow-workflow-builder/references/04-stage1-business-modeling.md +131 -0
  36. package/skills/qingflow-workflow-builder/references/05-stage2-members-roles.md +29 -0
  37. package/skills/qingflow-workflow-builder/references/06-stage3-build-spec.md +165 -0
  38. package/skills/qingflow-workflow-builder/references/07-stage4-validate-spec.md +33 -0
  39. package/skills/qingflow-workflow-builder/references/08-stage5-apply-verify.md +51 -0
  40. package/skills/qingflow-workflow-builder/references/09-stage6-summary.md +88 -0
  41. package/skills/qingflow-workflow-builder/references/10-node-config-reference.md +93 -0
  42. package/skills/qingflow-workflow-builder/references/11-troubleshooting.md +15 -0
  43. package/skills/qingflow-workflow-builder/scripts/diff_flow_spec.py +275 -0
  44. package/skills/qingflow-workflow-builder/scripts/validate_flow_spec.py +605 -0
  45. package/src/qingflow_mcp/__init__.py +1 -1
  46. package/src/qingflow_mcp/__main__.py +2 -6
  47. package/src/qingflow_mcp/builder_facade/models.py +108 -485
  48. package/src/qingflow_mcp/builder_facade/service.py +1686 -6674
  49. package/src/qingflow_mcp/cli/commands/builder.py +364 -322
  50. package/src/qingflow_mcp/cli/commands/chart.py +1 -1
  51. package/src/qingflow_mcp/cli/commands/common.py +3 -12
  52. package/src/qingflow_mcp/cli/commands/exports.py +2 -2
  53. package/src/qingflow_mcp/cli/commands/imports.py +3 -3
  54. package/src/qingflow_mcp/cli/commands/portal.py +2 -2
  55. package/src/qingflow_mcp/cli/commands/record.py +27 -101
  56. package/src/qingflow_mcp/cli/commands/task.py +47 -28
  57. package/src/qingflow_mcp/cli/commands/view.py +1 -1
  58. package/src/qingflow_mcp/cli/context.py +3 -0
  59. package/src/qingflow_mcp/cli/formatters.py +16 -784
  60. package/src/qingflow_mcp/cli/main.py +33 -117
  61. package/src/qingflow_mcp/errors.py +2 -43
  62. package/src/qingflow_mcp/public_surface.py +17 -26
  63. package/src/qingflow_mcp/response_trim.py +17 -81
  64. package/src/qingflow_mcp/server.py +12 -14
  65. package/src/qingflow_mcp/server_app_builder.py +36 -79
  66. package/src/qingflow_mcp/server_app_user.py +16 -22
  67. package/src/qingflow_mcp/session_store.py +7 -11
  68. package/src/qingflow_mcp/solution/compiler/__init__.py +1 -3
  69. package/src/qingflow_mcp/solution/executor.py +18 -245
  70. package/src/qingflow_mcp/tools/ai_builder_tools.py +1396 -3103
  71. package/src/qingflow_mcp/tools/app_tools.py +43 -184
  72. package/src/qingflow_mcp/tools/approval_tools.py +35 -197
  73. package/src/qingflow_mcp/tools/auth_tools.py +16 -92
  74. package/src/qingflow_mcp/tools/code_block_tools.py +40 -298
  75. package/src/qingflow_mcp/tools/custom_button_tools.py +10 -64
  76. package/src/qingflow_mcp/tools/directory_tools.py +72 -236
  77. package/src/qingflow_mcp/tools/export_tools.py +34 -244
  78. package/src/qingflow_mcp/tools/feedback_tools.py +0 -9
  79. package/src/qingflow_mcp/tools/file_tools.py +3 -9
  80. package/src/qingflow_mcp/tools/import_tools.py +49 -336
  81. package/src/qingflow_mcp/tools/navigation_tools.py +12 -91
  82. package/src/qingflow_mcp/tools/package_tools.py +6 -118
  83. package/src/qingflow_mcp/tools/portal_tools.py +3 -39
  84. package/src/qingflow_mcp/tools/qingbi_report_tools.py +7 -116
  85. package/src/qingflow_mcp/tools/record_tools.py +360 -1145
  86. package/src/qingflow_mcp/tools/resource_read_tools.py +39 -188
  87. package/src/qingflow_mcp/tools/role_tools.py +9 -80
  88. package/src/qingflow_mcp/tools/solution_tools.py +45 -59
  89. package/src/qingflow_mcp/tools/task_context_tools.py +158 -662
  90. package/src/qingflow_mcp/tools/task_tools.py +29 -113
  91. package/src/qingflow_mcp/tools/view_tools.py +3 -106
  92. package/src/qingflow_mcp/tools/workflow_tools.py +4 -48
  93. package/src/qingflow_mcp/tools/workspace_tools.py +3 -71
  94. package/npm/bin/qingflow-skills.mjs +0 -5
  95. package/skills/qingflow-app-builder/references/complete-system-development-guide.md +0 -172
  96. package/skills/qingflow-app-builder/references/single-app-development-guide.md +0 -87
  97. package/skills/qingflow-app-builder/scripts/validate_system_build_summary.py +0 -124
  98. package/src/qingflow_mcp/solution/compiler/workflow_compiler.py +0 -173
@@ -7,7 +7,7 @@ from mcp.server.fastmcp import FastMCP
7
7
 
8
8
  from ..backend_client import BackendRequestContext
9
9
  from ..config import DEFAULT_PROFILE
10
- from ..errors import QingflowApiError, backend_code_int, is_auth_like_error, raise_tool_error
10
+ from ..errors import QingflowApiError, raise_tool_error
11
11
  from ..json_types import JSONObject
12
12
  from ..list_type_labels import SYSTEM_VIEW_DEFINITIONS, get_app_publish_status_label
13
13
  from ..solution.compiler.icon_utils import workspace_icon_config
@@ -132,20 +132,6 @@ class AppTools(ToolBase):
132
132
  matched.append(item)
133
133
  return matched
134
134
 
135
- def _resolve_visible_app(self, context: BackendRequestContext, app_key: str) -> JSONObject | None:
136
- """Resolve app display metadata from the current user's visible app tree."""
137
- try:
138
- result = self.backend.request("GET", context, "/tag/apps")
139
- except QingflowApiError as exc:
140
- if not _is_optional_app_metadata_error(exc):
141
- raise
142
- return None
143
- items, _source_shape = self._extract_visible_apps(result)
144
- for item in items:
145
- if str(item.get("app_key") or "").strip() == app_key:
146
- return item
147
- return None
148
-
149
135
  @tool_cn_name("应用搜索")
150
136
  def app_search(self, *, profile: str, keyword: str = "", page_num: int = 1, page_size: int = 50) -> JSONObject:
151
137
  """Search apps by keyword in name/title using backend search API.
@@ -155,50 +141,9 @@ class AppTools(ToolBase):
155
141
  params: JSONObject = {"pageNum": page_num, "pageSize": page_size}
156
142
  if keyword:
157
143
  params["queryKey"] = keyword
158
-
159
- warnings: list[JSONObject] = []
160
- source = "app_item_search"
161
- try:
162
- result = self.backend.request("GET", context, "/app/item", params=params)
163
- except QingflowApiError as exc:
164
- if is_auth_like_error(exc):
165
- raise
166
- if not _is_optional_app_metadata_error(exc):
167
- raise
168
- visible_payload = self.backend.request("GET", context, "/tag/apps")
169
- all_apps, source_shape = self._extract_visible_apps(visible_payload)
170
- matched_apps = self._filter_visible_apps(all_apps, keyword) if keyword else all_apps
171
- page_start = max(page_num - 1, 0) * page_size
172
- page_end = page_start + page_size
173
- apps = matched_apps[page_start:page_end]
174
- warnings.append(
175
- {
176
- "code": "APP_SEARCH_FALLBACK_VISIBLE_APPS",
177
- "message": (
178
- "app_search used the current user's visible app tree because the global app search "
179
- "endpoint is unavailable in this permission context."
180
- ),
181
- "backend_code": exc.backend_code,
182
- "http_status": exc.http_status,
183
- "request_id": exc.request_id,
184
- }
185
- )
186
- return {
187
- "profile": profile,
188
- "ws_id": session_profile.selected_ws_id,
189
- "keyword": keyword,
190
- "page_num": page_num,
191
- "page_size": page_size,
192
- "total": len(matched_apps),
193
- "items": apps,
194
- "apps": apps,
195
- "source": "visible_apps_fallback",
196
- "source_shape": source_shape,
197
- "unfiltered_count": len(all_apps),
198
- "matched_count": len(matched_apps),
199
- "warnings": warnings,
200
- }
201
-
144
+
145
+ result = self.backend.request("GET", context, "/app/item", params=params)
146
+
202
147
  apps = []
203
148
  if isinstance(result, dict):
204
149
  items = result.get("list", [])
@@ -223,7 +168,6 @@ class AppTools(ToolBase):
223
168
  "total": result.get("total") if isinstance(result, dict) else len(apps),
224
169
  "items": apps,
225
170
  "apps": apps,
226
- "source": source,
227
171
  }
228
172
 
229
173
  return self._run(profile, runner)
@@ -238,7 +182,6 @@ class AppTools(ToolBase):
238
182
  app_name = app_key
239
183
  base_info: JSONObject | None = None
240
184
  app_icon = None
241
- visible_app: JSONObject | None = None
242
185
 
243
186
  try:
244
187
  base_info = self.backend.request("GET", context, f"/app/{app_key}/baseInfo")
@@ -249,31 +192,31 @@ class AppTools(ToolBase):
249
192
  )
250
193
  app_icon = str(base_info.get("appIcon") or "").strip() or None
251
194
  except QingflowApiError as exc:
252
- if not _is_optional_app_metadata_error(exc):
195
+ if exc.backend_code not in {40002, 40027}:
253
196
  raise
254
- visible_app = self._resolve_visible_app(context, app_key)
255
- if visible_app is not None:
256
- app_name = str(visible_app.get("app_name") or visible_app.get("title") or app_key).strip() or app_key
257
- app_icon = str(visible_app.get("app_icon") or "").strip() or None
258
197
  warnings.append(
259
- _with_api_error_fields(
260
- {
198
+ {
261
199
  "code": "APP_BASE_INFO_UNAVAILABLE",
200
+ "message": f"app_get could not load base info for {app_key}; using app_key as app_name.",
201
+ }
202
+ )
203
+
204
+ try:
205
+ can_create = self._probe_create_access(context, app_key)
206
+ except QingflowApiError as exc:
207
+ can_create = False
208
+ warnings.append(
209
+ {
210
+ "code": "APP_CREATE_PROBE_UNVERIFIED",
262
211
  "message": (
263
- f"app_get could not load base info for {app_key}; "
264
- "using the current user's visible app tree when available."
212
+ f"app_get could not fully verify create access for {app_key}: "
213
+ f"{exc.message or 'probe failed'}"
265
214
  ),
266
- },
267
- exc,
268
- )
215
+ }
269
216
  )
270
-
271
- can_create = self._probe_create_access(context, app_key)
272
217
  accessible_views, system_view_warnings = self._resolve_accessible_system_views(context, app_key)
273
218
  warnings.extend(system_view_warnings)
274
- custom_views, custom_view_warnings = self._resolve_accessible_custom_views(context, app_key)
275
- warnings.extend(custom_view_warnings)
276
- accessible_views.extend(custom_views)
219
+ accessible_views.extend(self._resolve_accessible_custom_views(context, app_key))
277
220
  import_capability, import_warnings = _derive_import_capability(base_info)
278
221
  warnings.extend(import_warnings)
279
222
  return {
@@ -287,7 +230,6 @@ class AppTools(ToolBase):
287
230
  "app_name": app_name,
288
231
  "app_icon": app_icon,
289
232
  "icon_config": workspace_icon_config(app_icon),
290
- **({"visible_app": visible_app} if base_info is None and visible_app is not None else {}),
291
233
  "can_create": can_create,
292
234
  "import_capability": import_capability,
293
235
  "accessible_views": accessible_views,
@@ -302,30 +244,7 @@ class AppTools(ToolBase):
302
244
  self._require_app_key(app_key)
303
245
 
304
246
  def runner(session_profile, context):
305
- warnings: list[JSONObject] = []
306
- verification: JSONObject = {"base_info_verified": True, "fallback_visible_app_used": False}
307
- try:
308
- result = self.backend.request("GET", context, f"/app/{app_key}/baseInfo")
309
- except QingflowApiError as exc:
310
- if not _is_optional_app_metadata_error(exc):
311
- raise
312
- visible_app = self._resolve_visible_app(context, app_key)
313
- if visible_app is None:
314
- raise
315
- result = self._base_info_from_visible_app(app_key, visible_app)
316
- warnings.append(
317
- _with_api_error_fields(
318
- {
319
- "code": "APP_BASE_INFO_UNAVAILABLE",
320
- "message": (
321
- f"app_get_base could not load base info for {app_key}; "
322
- "using the current user's visible app tree."
323
- ),
324
- },
325
- exc,
326
- )
327
- )
328
- verification = {"base_info_verified": False, "fallback_visible_app_used": True}
247
+ result = self.backend.request("GET", context, f"/app/{app_key}/baseInfo")
329
248
  publish_status = result.get("appPublishStatus") if isinstance(result, dict) else None
330
249
  compact = self._compact_base_info(result if isinstance(result, dict) else {})
331
250
  response = {
@@ -336,10 +255,7 @@ class AppTools(ToolBase):
336
255
  "app_publish_status": publish_status,
337
256
  "app_publish_status_label": get_app_publish_status_label(publish_status if isinstance(publish_status, int) else None),
338
257
  "compact": not include_raw,
339
- "verification": verification,
340
258
  }
341
- if warnings:
342
- response["warnings"] = warnings
343
259
  if include_raw:
344
260
  response["summary"] = compact
345
261
  return response
@@ -573,9 +489,7 @@ class AppTools(ToolBase):
573
489
  )
574
490
  return True
575
491
  except QingflowApiError as exc:
576
- if is_auth_like_error(exc):
577
- raise
578
- if backend_code_int(exc) in {40002, 40027, 404} or exc.http_status == 404:
492
+ if exc.backend_code in {40002, 40027}:
579
493
  return False
580
494
  raise
581
495
 
@@ -590,9 +504,7 @@ class AppTools(ToolBase):
590
504
  )
591
505
  return True
592
506
  except QingflowApiError as exc:
593
- if is_auth_like_error(exc):
594
- raise
595
- if backend_code_int(exc) in {40002, 40027, 404} or exc.http_status == 404:
507
+ if exc.backend_code in {40002, 40027}:
596
508
  return False
597
509
  raise
598
510
 
@@ -604,42 +516,40 @@ class AppTools(ToolBase):
604
516
  try:
605
517
  can_access = self._probe_list_type_access(context, app_key, list_type)
606
518
  except QingflowApiError as exc:
607
- raise
519
+ warnings.append(
520
+ {
521
+ "code": "SYSTEM_VIEW_PROBE_UNVERIFIED",
522
+ "message": (
523
+ f"app_get skipped system view '{name}' because access probing failed: "
524
+ f"{exc.message or 'probe failed'}"
525
+ ),
526
+ "view_id": view_id,
527
+ "list_type": list_type,
528
+ }
529
+ )
530
+ continue
608
531
  if not can_access:
609
532
  continue
610
533
  items.append({"view_id": view_id, "name": name, "kind": "system", "analysis_supported": True})
611
534
  return items, warnings
612
535
 
613
- def _resolve_accessible_custom_views(self, context: BackendRequestContext, app_key: str) -> tuple[list[JSONObject], list[JSONObject]]:
536
+ def _resolve_accessible_custom_views(self, context: BackendRequestContext, app_key: str) -> list[JSONObject]:
614
537
  """执行内部辅助逻辑。"""
615
- warnings: list[JSONObject] = []
616
538
  try:
617
539
  payload = self.backend.request("GET", context, f"/app/{app_key}/view/viewList")
618
540
  except QingflowApiError as exc:
619
- if _is_optional_app_metadata_error(exc):
620
- warnings.append(
621
- _with_api_error_fields(
622
- {
623
- "code": "CUSTOM_VIEW_LIST_UNAVAILABLE",
624
- "message": (
625
- f"app_get could not load custom views for {app_key}; "
626
- "system views and other app metadata may still be usable."
627
- ),
628
- },
629
- exc,
630
- )
631
- )
632
- return [], warnings
541
+ if exc.backend_code in {40002, 40027}:
542
+ return []
633
543
  raise
634
544
 
635
545
  items: list[JSONObject] = []
636
546
  for item in _normalize_view_list(payload):
637
- view_key = str(item.get("viewKey") or item.get("viewgraphKey") or "").strip()
547
+ view_key = str(item.get("viewKey") or "").strip()
638
548
  if not view_key:
639
549
  continue
640
550
  normalized: JSONObject = {
641
551
  "view_id": f"custom:{view_key}",
642
- "name": str(item.get("viewName") or item.get("viewgraphName") or view_key).strip() or view_key,
552
+ "name": str(item.get("viewName") or view_key).strip() or view_key,
643
553
  "kind": "custom",
644
554
  }
645
555
  view_type = str(item.get("viewType") or item.get("viewgraphType") or "").strip()
@@ -647,7 +557,7 @@ class AppTools(ToolBase):
647
557
  normalized["view_type"] = view_type
648
558
  normalized["analysis_supported"] = _analysis_supported_for_view_type(view_type or None)
649
559
  items.append(normalized)
650
- return items, warnings
560
+ return items
651
561
 
652
562
  def _compact_base_info(self, result: dict[str, Any]) -> JSONObject:
653
563
  """执行内部辅助逻辑。"""
@@ -691,30 +601,6 @@ class AppTools(ToolBase):
691
601
  },
692
602
  }
693
603
 
694
- def _base_info_from_visible_app(self, app_key: str, visible_app: JSONObject) -> JSONObject:
695
- """Build a base-info-shaped fallback from a current-user visible app item."""
696
- tag_ids: list[int] = []
697
- tag_id = _coerce_positive_int(visible_app.get("tag_id"))
698
- if tag_id is not None:
699
- tag_ids.append(tag_id)
700
- for value in visible_app.get("tag_ids") if isinstance(visible_app.get("tag_ids"), list) else []:
701
- tag_id = _coerce_positive_int(value)
702
- if tag_id is not None and tag_id not in tag_ids:
703
- tag_ids.append(tag_id)
704
-
705
- fallback: JSONObject = {
706
- "appKey": app_key,
707
- "formId": visible_app.get("form_id"),
708
- "formTitle": visible_app.get("app_name") or visible_app.get("title") or app_key,
709
- "appIcon": visible_app.get("app_icon"),
710
- "tagIds": tag_ids,
711
- "visibleApp": visible_app,
712
- }
713
- package_name = visible_app.get("package_name") or visible_app.get("tag_name") or visible_app.get("group_name")
714
- if package_name:
715
- fallback["tagItems"] = [{"tagId": tag_id, "tagName": package_name} for tag_id in tag_ids]
716
- return fallback
717
-
718
604
  def _compact_form_schema(self, result: dict[str, Any]) -> JSONObject:
719
605
  """执行内部辅助逻辑。"""
720
606
  base_questions_raw = result.get("baseQues") if isinstance(result.get("baseQues"), list) else []
@@ -873,7 +759,6 @@ class AppTools(ToolBase):
873
759
  "app_key": app_key,
874
760
  "app_name": title,
875
761
  "title": title,
876
- "app_icon": str(item.get("appIcon") or "").strip() or None,
877
762
  "form_id": item.get("formId"),
878
763
  "tag_id": package_tag_id,
879
764
  "package_name": package_name,
@@ -934,26 +819,17 @@ def _normalize_form_type(value: int | str) -> int:
934
819
 
935
820
 
936
821
  def _normalize_view_list(payload: Any) -> list[JSONObject]:
937
- if isinstance(payload, dict):
938
- raw_list = payload.get("list")
939
- if isinstance(raw_list, list):
940
- payload = raw_list
941
- else:
942
- payload = [payload]
943
822
  if not isinstance(payload, list):
944
823
  return []
945
824
  flattened: list[JSONObject] = []
946
825
  for group in payload:
947
826
  if not isinstance(group, dict):
948
827
  continue
949
- if group.get("viewKey") or group.get("viewgraphKey"):
950
- flattened.append(group)
951
- continue
952
828
  view_list = group.get("viewList")
953
829
  if not isinstance(view_list, list):
954
830
  continue
955
831
  for item in view_list:
956
- if isinstance(item, dict) and (item.get("viewKey") or item.get("viewgraphKey")):
832
+ if isinstance(item, dict) and item.get("viewKey"):
957
833
  flattened.append(item)
958
834
  return flattened
959
835
 
@@ -965,23 +841,6 @@ def _analysis_supported_for_view_type(view_type: str | None) -> bool:
965
841
  return normalized not in {"boardview", "ganttview"}
966
842
 
967
843
 
968
- def _is_optional_app_metadata_error(error: QingflowApiError) -> bool:
969
- if is_auth_like_error(error):
970
- return False
971
- backend_code = backend_code_int(error)
972
- return backend_code in {40002, 40027, 404} or error.http_status == 404
973
-
974
-
975
- def _with_api_error_fields(payload: JSONObject, error: QingflowApiError) -> JSONObject:
976
- if error.backend_code is not None:
977
- payload["backend_code"] = error.backend_code
978
- if error.http_status is not None:
979
- payload["http_status"] = error.http_status
980
- if error.request_id:
981
- payload["request_id"] = error.request_id
982
- return payload
983
-
984
-
985
844
  def _derive_import_capability(base_info: Any) -> tuple[JSONObject, list[JSONObject]]:
986
845
  warnings: list[JSONObject] = []
987
846
  if not isinstance(base_info, dict):