@qingflow-tech/qingflow-app-user-mcp 1.0.2 → 1.0.4

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 (56) hide show
  1. package/README.md +2 -2
  2. package/docs/local-agent-install.md +9 -3
  3. package/npm/lib/runtime.mjs +10 -3
  4. package/package.json +1 -1
  5. package/pyproject.toml +1 -1
  6. package/skills/qingflow-app-user/SKILL.md +21 -12
  7. package/skills/qingflow-app-user/references/data-gotchas.md +1 -1
  8. package/skills/qingflow-app-user/references/public-surface-sync.md +70 -0
  9. package/skills/qingflow-app-user/references/record-patterns.md +1 -1
  10. package/skills/qingflow-record-analysis/SKILL.md +44 -2
  11. package/skills/qingflow-record-insert/SKILL.md +3 -0
  12. package/skills/qingflow-record-update/SKILL.md +3 -0
  13. package/skills/qingflow-task-ops/SKILL.md +31 -10
  14. package/src/qingflow_mcp/__init__.py +33 -1
  15. package/src/qingflow_mcp/backend_client.py +109 -0
  16. package/src/qingflow_mcp/builder_facade/button_style_catalog.py +282 -0
  17. package/src/qingflow_mcp/builder_facade/models.py +58 -9
  18. package/src/qingflow_mcp/builder_facade/service.py +1711 -240
  19. package/src/qingflow_mcp/cli/commands/__init__.py +2 -1
  20. package/src/qingflow_mcp/cli/commands/app.py +47 -1
  21. package/src/qingflow_mcp/cli/commands/auth.py +63 -0
  22. package/src/qingflow_mcp/cli/commands/builder.py +11 -3
  23. package/src/qingflow_mcp/cli/commands/exports.py +111 -0
  24. package/src/qingflow_mcp/cli/commands/record.py +5 -5
  25. package/src/qingflow_mcp/cli/commands/task.py +701 -27
  26. package/src/qingflow_mcp/cli/commands/workspace.py +84 -0
  27. package/src/qingflow_mcp/cli/context.py +3 -0
  28. package/src/qingflow_mcp/cli/formatters.py +424 -50
  29. package/src/qingflow_mcp/cli/interaction.py +72 -0
  30. package/src/qingflow_mcp/cli/main.py +11 -1
  31. package/src/qingflow_mcp/cli/qingflow_login.py +116 -0
  32. package/src/qingflow_mcp/cli/terminal_ui.py +218 -0
  33. package/src/qingflow_mcp/config.py +1 -1
  34. package/src/qingflow_mcp/errors.py +4 -4
  35. package/src/qingflow_mcp/export_store.py +14 -0
  36. package/src/qingflow_mcp/id_utils.py +49 -0
  37. package/src/qingflow_mcp/public_surface.py +16 -1
  38. package/src/qingflow_mcp/response_trim.py +394 -9
  39. package/src/qingflow_mcp/server.py +26 -0
  40. package/src/qingflow_mcp/server_app_builder.py +15 -1
  41. package/src/qingflow_mcp/server_app_user.py +113 -0
  42. package/src/qingflow_mcp/session_store.py +126 -21
  43. package/src/qingflow_mcp/solution/compiler/form_compiler.py +2 -2
  44. package/src/qingflow_mcp/solution/executor.py +2 -2
  45. package/src/qingflow_mcp/tools/ai_builder_tools.py +107 -34
  46. package/src/qingflow_mcp/tools/app_tools.py +1 -0
  47. package/src/qingflow_mcp/tools/auth_tools.py +243 -9
  48. package/src/qingflow_mcp/tools/base.py +6 -2
  49. package/src/qingflow_mcp/tools/code_block_tools.py +2 -2
  50. package/src/qingflow_mcp/tools/custom_button_tools.py +0 -2
  51. package/src/qingflow_mcp/tools/export_tools.py +1565 -0
  52. package/src/qingflow_mcp/tools/import_tools.py +78 -4
  53. package/src/qingflow_mcp/tools/record_tools.py +551 -165
  54. package/src/qingflow_mcp/tools/resource_read_tools.py +154 -33
  55. package/src/qingflow_mcp/tools/task_context_tools.py +917 -141
  56. package/src/qingflow_mcp/tools/workspace_tools.py +141 -0
@@ -0,0 +1,282 @@
1
+ from __future__ import annotations
2
+
3
+ from copy import deepcopy
4
+ from typing import Any
5
+
6
+ BUTTON_BACKGROUND_COLORS: tuple[str, ...] = (
7
+ "transparent",
8
+ "#FB9337",
9
+ "#FA6F32",
10
+ "#FAB300",
11
+ "#67C200",
12
+ "#00BD77",
13
+ "#00C5FB",
14
+ "#268BFB",
15
+ "#001A72",
16
+ "#9E64FB",
17
+ "#D164FB",
18
+ "#FB4B51",
19
+ "#FFFFFF",
20
+ )
21
+
22
+ BUTTON_TEXT_COLORS: tuple[str, ...] = (
23
+ "#FB9337",
24
+ "#FA6F32",
25
+ "#FAB300",
26
+ "#67C200",
27
+ "#00BD77",
28
+ "#00C5FB",
29
+ "#268BFB",
30
+ "#001A72",
31
+ "#9E64FB",
32
+ "#D164FB",
33
+ "#FB4B51",
34
+ "#494F57",
35
+ "#FFFFFF",
36
+ )
37
+
38
+ BUTTON_ICONS: tuple[str, ...] = (
39
+ "ex-print",
40
+ "ex-attachment",
41
+ "ex-handoff",
42
+ "ex-delete",
43
+ "ex-copy",
44
+ "ex-share",
45
+ "ex-edit2",
46
+ "ex-admin-outlined",
47
+ "ex-stamp",
48
+ "ex-kanban",
49
+ "ex-update",
50
+ "ex-tickincircle-outlined",
51
+ "ex-crossincircle-outlined",
52
+ "ex-plus-circle",
53
+ "ex-logout",
54
+ "ex-check",
55
+ "ex-rename",
56
+ "ex-locked",
57
+ "ex-shift",
58
+ "ex-new-tabpage",
59
+ "ex-cross",
60
+ "ex-switch",
61
+ "ex-layer",
62
+ "ex-import",
63
+ "ex-pin-outlined",
64
+ "ex-disabled",
65
+ "ex-duplicate",
66
+ "ex-heart-outlined",
67
+ "ex-plus",
68
+ "ex-save",
69
+ "ex-upload",
70
+ "ex-upgrade1",
71
+ "ex-downgrade",
72
+ "ex-unfold",
73
+ "ex-fold",
74
+ "ex-sort",
75
+ "ex-menu-control",
76
+ "ex-download",
77
+ "ex-insert-below",
78
+ "ex-left-outlined-double",
79
+ "ex-right-outlined-double",
80
+ "ex-recovery",
81
+ "ex-layout",
82
+ "ex-search",
83
+ "ex-preview",
84
+ "ex-invisible",
85
+ "ex-carboncopy",
86
+ "ex-basicInfo",
87
+ "ex-fillIn",
88
+ "ex-refresh",
89
+ "ex-display",
90
+ "ex-message",
91
+ "ex-edit",
92
+ "ex-heart-filled",
93
+ "ex-pin-filled",
94
+ "ex-transfer",
95
+ "ex-cross-circle",
96
+ "ex-clock",
97
+ "ex-admin-filled",
98
+ "ex-tickincircle-circle",
99
+ "ex-all-application",
100
+ "ex-help-filled",
101
+ "ex-streamline",
102
+ "ex-table",
103
+ "ex-rowheight-short",
104
+ "ex-rowheight-medium",
105
+ "ex-rowheight-tall",
106
+ "ex-rowheight-tallest",
107
+ )
108
+
109
+ BUTTON_STYLE_PRESETS: tuple[dict[str, Any], ...] = (
110
+ {
111
+ "key": "primary_blue",
112
+ "label": "Primary Blue",
113
+ "button_type": "default",
114
+ "background_color": "#268BFB",
115
+ "text_color": "#FFFFFF",
116
+ "recommended_icons": ["ex-plus-circle", "ex-plus"],
117
+ },
118
+ {
119
+ "key": "text_blue",
120
+ "label": "Text Blue",
121
+ "button_type": "text",
122
+ "background_color": "transparent",
123
+ "text_color": "#268BFB",
124
+ "recommended_icons": ["ex-share", "ex-new-tabpage"],
125
+ },
126
+ {
127
+ "key": "warning_orange",
128
+ "label": "Warning Orange",
129
+ "button_type": "default",
130
+ "background_color": "#FB9337",
131
+ "text_color": "#FFFFFF",
132
+ "recommended_icons": ["ex-message", "ex-clock"],
133
+ },
134
+ {
135
+ "key": "danger_red",
136
+ "label": "Danger Red",
137
+ "button_type": "default",
138
+ "background_color": "#FB4B51",
139
+ "text_color": "#FFFFFF",
140
+ "recommended_icons": ["ex-delete", "ex-cross-circle"],
141
+ },
142
+ {
143
+ "key": "neutral_outline",
144
+ "label": "Neutral Outline",
145
+ "button_type": "default",
146
+ "background_color": "#FFFFFF",
147
+ "text_color": "#494F57",
148
+ "recommended_icons": ["ex-edit", "ex-search"],
149
+ },
150
+ {
151
+ "key": "secondary_gray",
152
+ "label": "Secondary Gray",
153
+ "button_type": "text",
154
+ "background_color": "transparent",
155
+ "text_color": "#494F57",
156
+ "recommended_icons": ["ex-edit", "ex-search"],
157
+ },
158
+ )
159
+
160
+ _PRESET_BY_KEY: dict[str, dict[str, Any]] = {
161
+ str(item["key"]).strip(): dict(item) for item in BUTTON_STYLE_PRESETS
162
+ }
163
+
164
+ _COLOR_FAMILY_BY_VALUE: dict[str, str] = {
165
+ "transparent": "neutral",
166
+ "#FB9337": "orange",
167
+ "#FA6F32": "orange",
168
+ "#FAB300": "yellow",
169
+ "#67C200": "green",
170
+ "#00BD77": "green",
171
+ "#00C5FB": "cyan",
172
+ "#268BFB": "blue",
173
+ "#001A72": "blue",
174
+ "#9E64FB": "purple",
175
+ "#D164FB": "purple",
176
+ "#FB4B51": "red",
177
+ "#FFFFFF": "white",
178
+ "#494F57": "gray",
179
+ }
180
+
181
+
182
+ def _normalize_color_value(value: str | None) -> str | None:
183
+ normalized = str(value or "").strip()
184
+ if not normalized:
185
+ return None
186
+ lowered = normalized.lower().replace(" ", "")
187
+ if lowered == "transparent":
188
+ return "transparent"
189
+ if lowered in {
190
+ "#fff",
191
+ "#ffffff",
192
+ "white",
193
+ "rgb(255,255,255)",
194
+ "rgba(255,255,255,1)",
195
+ }:
196
+ return "#FFFFFF"
197
+ if normalized.startswith("#"):
198
+ return normalized.upper()
199
+ return normalized
200
+
201
+
202
+ def button_style_catalog_payload() -> dict[str, Any]:
203
+ return {
204
+ "icons": [{"value": icon, "label": icon} for icon in BUTTON_ICONS],
205
+ "background_colors": [
206
+ {
207
+ "value": color,
208
+ "label": color,
209
+ "family": _COLOR_FAMILY_BY_VALUE.get(color, "custom"),
210
+ }
211
+ for color in BUTTON_BACKGROUND_COLORS
212
+ ],
213
+ "text_colors": [
214
+ {
215
+ "value": color,
216
+ "label": color,
217
+ "family": _COLOR_FAMILY_BY_VALUE.get(color, "custom"),
218
+ }
219
+ for color in BUTTON_TEXT_COLORS
220
+ ],
221
+ "presets": [deepcopy(item) for item in BUTTON_STYLE_PRESETS],
222
+ }
223
+
224
+
225
+ def resolve_button_style(
226
+ *,
227
+ style_preset: str | None,
228
+ button_icon: str | None,
229
+ background_color: str | None,
230
+ text_color: str | None,
231
+ require_complete_style: bool,
232
+ ) -> dict[str, str | None]:
233
+ preset_key = str(style_preset or "").strip() or None
234
+ resolved_icon = str(button_icon or "").strip() or None
235
+ resolved_background = _normalize_color_value(background_color)
236
+ resolved_text = _normalize_color_value(text_color)
237
+
238
+ if preset_key is not None:
239
+ preset = _PRESET_BY_KEY.get(preset_key)
240
+ if preset is None:
241
+ raise ValueError(
242
+ "unsupported style_preset; use button_style_catalog_get to inspect available presets"
243
+ )
244
+ if resolved_background is None:
245
+ resolved_background = str(preset["background_color"])
246
+ if resolved_text is None:
247
+ resolved_text = str(preset["text_color"])
248
+ if resolved_icon is None:
249
+ recommended = preset.get("recommended_icons") or []
250
+ if recommended:
251
+ resolved_icon = str(recommended[0]).strip() or None
252
+
253
+ if require_complete_style:
254
+ missing: list[str] = []
255
+ if resolved_icon is None:
256
+ missing.append("button_icon")
257
+ if resolved_background is None:
258
+ missing.append("background_color")
259
+ if resolved_text is None:
260
+ missing.append("text_color")
261
+ if missing:
262
+ raise ValueError(f"button style requires {', '.join(missing)}")
263
+
264
+ if resolved_icon is not None and resolved_icon not in BUTTON_ICONS:
265
+ raise ValueError(
266
+ "unsupported button_icon; use button_style_catalog_get to inspect available icons"
267
+ )
268
+ if resolved_background is not None and resolved_background not in BUTTON_BACKGROUND_COLORS:
269
+ raise ValueError(
270
+ "unsupported background_color; current frontend only supports template colors from button_style_catalog_get"
271
+ )
272
+ if resolved_text is not None and resolved_text not in BUTTON_TEXT_COLORS:
273
+ raise ValueError(
274
+ "unsupported text_color; current frontend only supports template colors from button_style_catalog_get"
275
+ )
276
+
277
+ return {
278
+ "style_preset": preset_key,
279
+ "button_icon": resolved_icon,
280
+ "background_color": resolved_background,
281
+ "text_color": resolved_text,
282
+ }
@@ -5,6 +5,7 @@ from typing import Any
5
5
 
6
6
  from pydantic import AliasChoices, Field, model_validator
7
7
 
8
+ from .button_style_catalog import resolve_button_style
8
9
  from ..solution.spec_models import StrictModel
9
10
 
10
11
 
@@ -819,6 +820,10 @@ class FieldMutation(StrictModel):
819
820
  validation_alias=AliasChoices("custom_button_text", "customButtonText", "custom_btn_text", "customBtnText"),
820
821
  )
821
822
  subfields: list[FieldPatch] | None = None
823
+ subfield_updates: list["FieldUpdatePatch"] | None = Field(
824
+ default=None,
825
+ validation_alias=AliasChoices("subfield_updates", "subfieldUpdates"),
826
+ )
822
827
 
823
828
  @model_validator(mode="after")
824
829
  def validate_shape(self) -> "FieldMutation":
@@ -848,8 +853,12 @@ class FieldMutation(StrictModel):
848
853
  or self.custom_button_text is not None
849
854
  ):
850
855
  raise ValueError("code_block_config, code_block_binding, auto_trigger, custom_button_text_enabled, and custom_button_text are only allowed for code_block fields")
851
- if self.type == PublicFieldType.subtable and not self.subfields:
852
- raise ValueError("subtable field requires subfields")
856
+ if self.type == PublicFieldType.subtable and not self.subfields and not self.subfield_updates:
857
+ raise ValueError("subtable field requires subfields or subfield_updates")
858
+ if self.type is not None and self.type != PublicFieldType.subtable and self.subfield_updates:
859
+ raise ValueError("subfield_updates are only allowed for subtable fields")
860
+ if self.subfields and self.subfield_updates:
861
+ raise ValueError("subfields and subfield_updates cannot be used together")
853
862
  return self
854
863
 
855
864
  @model_validator(mode="before")
@@ -1172,10 +1181,10 @@ def _is_white_button_color(value: str | None) -> bool:
1172
1181
 
1173
1182
  class CustomButtonPatch(StrictModel):
1174
1183
  button_text: str = Field(validation_alias=AliasChoices("button_text", "buttonText"))
1175
- background_color: str = Field(validation_alias=AliasChoices("background_color", "backgroundColor"))
1176
- text_color: str = Field(validation_alias=AliasChoices("text_color", "textColor"))
1177
- button_icon: str = Field(validation_alias=AliasChoices("button_icon", "buttonIcon"))
1178
- icon_color: str | None = Field(default=None, validation_alias=AliasChoices("icon_color", "iconColor"))
1184
+ background_color: str | None = Field(default=None, validation_alias=AliasChoices("background_color", "backgroundColor"))
1185
+ text_color: str | None = Field(default=None, validation_alias=AliasChoices("text_color", "textColor"))
1186
+ button_icon: str | None = Field(default=None, validation_alias=AliasChoices("button_icon", "buttonIcon"))
1187
+ style_preset: str | None = Field(default=None, validation_alias=AliasChoices("style_preset", "stylePreset"))
1179
1188
  trigger_action: PublicButtonTriggerAction = Field(validation_alias=AliasChoices("trigger_action", "triggerAction"))
1180
1189
  trigger_link_url: str | None = Field(default=None, validation_alias=AliasChoices("trigger_link_url", "triggerLinkUrl"))
1181
1190
  trigger_add_data_config: CustomButtonAddDataConfigPatch | None = Field(
@@ -1196,8 +1205,29 @@ class CustomButtonPatch(StrictModel):
1196
1205
  validation_alias=AliasChoices("trigger_wings_config", "triggerWingsConfig"),
1197
1206
  )
1198
1207
 
1208
+ @model_validator(mode="before")
1209
+ @classmethod
1210
+ def normalize_aliases(cls, value: Any) -> Any:
1211
+ if not isinstance(value, dict):
1212
+ return value
1213
+ payload = dict(value)
1214
+ if "icon_color" in payload or "iconColor" in payload:
1215
+ raise ValueError("icon_color is not supported; icon color follows text_color")
1216
+ return payload
1217
+
1199
1218
  @model_validator(mode="after")
1200
1219
  def validate_shape(self) -> "CustomButtonPatch":
1220
+ resolved = resolve_button_style(
1221
+ style_preset=self.style_preset,
1222
+ button_icon=self.button_icon,
1223
+ background_color=self.background_color,
1224
+ text_color=self.text_color,
1225
+ require_complete_style=True,
1226
+ )
1227
+ self.style_preset = resolved["style_preset"]
1228
+ self.button_icon = resolved["button_icon"]
1229
+ self.background_color = resolved["background_color"]
1230
+ self.text_color = resolved["text_color"]
1201
1231
  if self.trigger_action == PublicButtonTriggerAction.link and not str(self.trigger_link_url or "").strip():
1202
1232
  raise ValueError("link buttons require trigger_link_url")
1203
1233
  if self.trigger_action == PublicButtonTriggerAction.add_data and self.trigger_add_data_config is None:
@@ -1217,7 +1247,7 @@ class ViewButtonBindingPatch(StrictModel):
1217
1247
  button_id: int = Field(validation_alias=AliasChoices("button_id", "buttonId", "id"))
1218
1248
  button_text: str | None = Field(default=None, validation_alias=AliasChoices("button_text", "buttonText"))
1219
1249
  button_icon: str | None = Field(default=None, validation_alias=AliasChoices("button_icon", "buttonIcon"))
1220
- icon_color: str | None = Field(default=None, validation_alias=AliasChoices("icon_color", "iconColor"))
1250
+ style_preset: str | None = Field(default=None, validation_alias=AliasChoices("style_preset", "stylePreset"))
1221
1251
  background_color: str | None = Field(default=None, validation_alias=AliasChoices("background_color", "backgroundColor"))
1222
1252
  text_color: str | None = Field(default=None, validation_alias=AliasChoices("text_color", "textColor"))
1223
1253
  trigger_action: str | None = Field(default=None, validation_alias=AliasChoices("trigger_action", "triggerAction"))
@@ -1236,6 +1266,8 @@ class ViewButtonBindingPatch(StrictModel):
1236
1266
  if not isinstance(value, dict):
1237
1267
  return value
1238
1268
  payload = dict(value)
1269
+ if "icon_color" in payload or "iconColor" in payload:
1270
+ raise ValueError("icon_color is not supported; icon color follows text_color")
1239
1271
  raw_button_type = payload.get("button_type", payload.get("buttonType"))
1240
1272
  if isinstance(raw_button_type, str):
1241
1273
  normalized_type = raw_button_type.strip().lower()
@@ -1257,6 +1289,21 @@ class ViewButtonBindingPatch(StrictModel):
1257
1289
 
1258
1290
  @model_validator(mode="after")
1259
1291
  def validate_shape(self) -> "ViewButtonBindingPatch":
1292
+ require_complete_style = self.button_type == PublicViewButtonType.system or any(
1293
+ str(value or "").strip()
1294
+ for value in (self.style_preset, self.button_icon, self.background_color, self.text_color)
1295
+ )
1296
+ resolved = resolve_button_style(
1297
+ style_preset=self.style_preset,
1298
+ button_icon=self.button_icon,
1299
+ background_color=self.background_color,
1300
+ text_color=self.text_color,
1301
+ require_complete_style=require_complete_style,
1302
+ )
1303
+ self.style_preset = resolved["style_preset"]
1304
+ self.button_icon = resolved["button_icon"]
1305
+ self.background_color = resolved["background_color"]
1306
+ self.text_color = resolved["text_color"]
1260
1307
  if self.button_type == PublicViewButtonType.system:
1261
1308
  missing = [
1262
1309
  field_name
@@ -1528,14 +1575,16 @@ class PortalApplyRequest(StrictModel):
1528
1575
  raise ValueError("package_tag_id is required when dash_key is empty")
1529
1576
  if not self.dash_key and not self.dash_name:
1530
1577
  raise ValueError("dash_name is required when creating a portal")
1531
- if not self.sections:
1532
- raise ValueError("portal apply requires a non-empty sections list")
1578
+ if not self.dash_key and not self.sections:
1579
+ raise ValueError("portal apply requires a non-empty sections list when creating a portal")
1533
1580
  if self.visibility is not None and self.auth is not None:
1534
1581
  raise ValueError("visibility and auth cannot be provided together")
1535
1582
  return self
1536
1583
 
1537
1584
 
1538
1585
  FieldPatch.model_rebuild()
1586
+ FieldMutation.model_rebuild()
1587
+ FieldUpdatePatch.model_rebuild()
1539
1588
 
1540
1589
 
1541
1590
  class AppGetResponse(StrictModel):