@josephyan/qingflow-app-builder-mcp 0.2.0-beta.49 → 0.2.0-beta.50

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.
package/README.md CHANGED
@@ -3,13 +3,13 @@
3
3
  Install:
4
4
 
5
5
  ```bash
6
- npm install @josephyan/qingflow-app-builder-mcp@0.2.0-beta.49
6
+ npm install @josephyan/qingflow-app-builder-mcp@0.2.0-beta.50
7
7
  ```
8
8
 
9
9
  Run:
10
10
 
11
11
  ```bash
12
- npx -y -p @josephyan/qingflow-app-builder-mcp@0.2.0-beta.49 qingflow-app-builder-mcp
12
+ npx -y -p @josephyan/qingflow-app-builder-mcp@0.2.0-beta.50 qingflow-app-builder-mcp
13
13
  ```
14
14
 
15
15
  Environment:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@josephyan/qingflow-app-builder-mcp",
3
- "version": "0.2.0-beta.49",
3
+ "version": "0.2.0-beta.50",
4
4
  "description": "Builder MCP for Qingflow app/package/system design and staged solution workflows.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/pyproject.toml CHANGED
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "qingflow-mcp"
7
- version = "0.2.0b49"
7
+ version = "0.2.0b50"
8
8
  description = "User-authenticated MCP server for Qingflow"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -28,6 +28,11 @@ class PublicFieldType(str, Enum):
28
28
  subtable = "subtable"
29
29
 
30
30
 
31
+ class PublicRelationMode(str, Enum):
32
+ single = "single"
33
+ multiple = "multiple"
34
+
35
+
31
36
  FIELD_TYPE_ALIASES: dict[str, PublicFieldType] = {
32
37
  "textarea": PublicFieldType.long_text,
33
38
  "amount": PublicFieldType.amount,
@@ -268,6 +273,7 @@ class FieldPatch(StrictModel):
268
273
  target_app_key: str | None = None
269
274
  display_field: FieldSelector | None = None
270
275
  visible_fields: list[FieldSelector] = Field(default_factory=list)
276
+ relation_mode: PublicRelationMode | None = Field(default=None, validation_alias=AliasChoices("relation_mode", "relationMode", "selection_mode", "selectionMode"))
271
277
  subfields: list["FieldPatch"] = Field(default_factory=list)
272
278
 
273
279
  @model_validator(mode="after")
@@ -276,8 +282,8 @@ class FieldPatch(StrictModel):
276
282
  raise ValueError("relation field requires target_app_key")
277
283
  if self.type != PublicFieldType.relation and self.target_app_key:
278
284
  raise ValueError("target_app_key is only allowed for relation fields")
279
- if self.type != PublicFieldType.relation and (self.display_field is not None or self.visible_fields):
280
- raise ValueError("display_field and visible_fields are only allowed for relation fields")
285
+ if self.type != PublicFieldType.relation and (self.display_field is not None or self.visible_fields or self.relation_mode is not None):
286
+ raise ValueError("display_field, visible_fields, and relation_mode are only allowed for relation fields")
281
287
  if self.type == PublicFieldType.subtable and not self.subfields:
282
288
  raise ValueError("subtable field requires subfields")
283
289
  if self.type != PublicFieldType.subtable and self.subfields:
@@ -299,14 +305,15 @@ class FieldMutation(StrictModel):
299
305
  target_app_key: str | None = None
300
306
  display_field: FieldSelector | None = None
301
307
  visible_fields: list[FieldSelector] | None = None
308
+ relation_mode: PublicRelationMode | None = Field(default=None, validation_alias=AliasChoices("relation_mode", "relationMode", "selection_mode", "selectionMode"))
302
309
  subfields: list[FieldPatch] | None = None
303
310
 
304
311
  @model_validator(mode="after")
305
312
  def validate_shape(self) -> "FieldMutation":
306
313
  if self.type == PublicFieldType.relation and not self.target_app_key:
307
314
  raise ValueError("relation field requires target_app_key")
308
- if self.type is not None and self.type != PublicFieldType.relation and (self.display_field is not None or self.visible_fields):
309
- raise ValueError("display_field and visible_fields are only allowed for relation fields")
315
+ if self.type is not None and self.type != PublicFieldType.relation and (self.display_field is not None or self.visible_fields or self.relation_mode is not None):
316
+ raise ValueError("display_field, visible_fields, and relation_mode are only allowed for relation fields")
310
317
  if self.type == PublicFieldType.subtable and not self.subfields:
311
318
  raise ValueError("subtable field requires subfields")
312
319
  return self
@@ -915,11 +922,30 @@ def _normalize_field_payload(value: Any) -> Any:
915
922
  normalized_from_id = FIELD_TYPE_ID_ALIASES.get(raw_type)
916
923
  if normalized_from_id is not None:
917
924
  payload["type"] = normalized_from_id.value
918
- return payload
919
925
  if isinstance(raw_type, str):
920
926
  normalized = FIELD_TYPE_ALIASES.get(raw_type.strip().lower())
921
927
  if normalized is not None:
922
928
  payload["type"] = normalized.value
929
+ normalized_relation_mode = _normalize_public_relation_mode(
930
+ payload.get("relation_mode", payload.get("relationMode", payload.get("selection_mode", payload.get("selectionMode"))))
931
+ )
932
+ if normalized_relation_mode is None:
933
+ for alias_key in ("optional_data_num", "optionalDataNum", "multiple", "allow_multiple"):
934
+ if alias_key in payload:
935
+ normalized_relation_mode = _normalize_public_relation_mode(payload.get(alias_key))
936
+ break
937
+ if normalized_relation_mode is not None:
938
+ payload["relation_mode"] = normalized_relation_mode
939
+ for alias_key in (
940
+ "relationMode",
941
+ "selection_mode",
942
+ "selectionMode",
943
+ "optional_data_num",
944
+ "optionalDataNum",
945
+ "multiple",
946
+ "allow_multiple",
947
+ ):
948
+ payload.pop(alias_key, None)
923
949
  return payload
924
950
 
925
951
 
@@ -927,3 +953,33 @@ def _slugify_title(title: str) -> str:
927
953
  normalized = "".join(ch.lower() if ch.isalnum() else "_" for ch in str(title or ""))
928
954
  collapsed = "_".join(part for part in normalized.split("_") if part)
929
955
  return collapsed or "section"
956
+
957
+
958
+ def _normalize_public_relation_mode(value: Any) -> str | None:
959
+ if value is None:
960
+ return None
961
+ if isinstance(value, bool):
962
+ return PublicRelationMode.multiple.value if value else PublicRelationMode.single.value
963
+ if isinstance(value, int):
964
+ if value == 0:
965
+ return PublicRelationMode.multiple.value
966
+ if value == 1:
967
+ return PublicRelationMode.single.value
968
+ return None
969
+ if isinstance(value, str):
970
+ normalized = value.strip().lower()
971
+ aliases = {
972
+ "single": PublicRelationMode.single.value,
973
+ "single_select": PublicRelationMode.single.value,
974
+ "single-select": PublicRelationMode.single.value,
975
+ "one": PublicRelationMode.single.value,
976
+ "1": PublicRelationMode.single.value,
977
+ "multiple": PublicRelationMode.multiple.value,
978
+ "multi": PublicRelationMode.multiple.value,
979
+ "multi_select": PublicRelationMode.multiple.value,
980
+ "multi-select": PublicRelationMode.multiple.value,
981
+ "many": PublicRelationMode.multiple.value,
982
+ "0": PublicRelationMode.multiple.value,
983
+ }
984
+ return aliases.get(normalized, normalized or None)
985
+ return None