@josephyan/qingflow-app-user-mcp 0.2.0-beta.37 → 0.2.0-beta.39

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-user-mcp@0.2.0-beta.37
6
+ npm install @josephyan/qingflow-app-user-mcp@0.2.0-beta.39
7
7
  ```
8
8
 
9
9
  Run:
10
10
 
11
11
  ```bash
12
- npx -y -p @josephyan/qingflow-app-user-mcp@0.2.0-beta.37 qingflow-app-user-mcp
12
+ npx -y -p @josephyan/qingflow-app-user-mcp@0.2.0-beta.39 qingflow-app-user-mcp
13
13
  ```
14
14
 
15
15
  Environment:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@josephyan/qingflow-app-user-mcp",
3
- "version": "0.2.0-beta.37",
3
+ "version": "0.2.0-beta.39",
4
4
  "description": "Operational end-user MCP for Qingflow records, tasks, comments, and directory 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.0b37"
7
+ version = "0.2.0b39"
8
8
  description = "User-authenticated MCP server for Qingflow"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -2,4 +2,4 @@ from __future__ import annotations
2
2
 
3
3
  __all__ = ["__version__"]
4
4
 
5
- __version__ = "0.2.0b37"
5
+ __version__ = "0.2.0b39"
@@ -69,6 +69,14 @@ class PublicViewType(str, Enum):
69
69
  gantt = "gantt"
70
70
 
71
71
 
72
+ class PublicChartType(str, Enum):
73
+ target = "target"
74
+ pie = "pie"
75
+ bar = "bar"
76
+ line = "line"
77
+ table = "table"
78
+
79
+
72
80
  class LayoutApplyMode(str, Enum):
73
81
  merge = "merge"
74
82
  replace = "replace"
@@ -491,6 +499,261 @@ class ViewUpsertPatch(StrictModel):
491
499
  return self
492
500
 
493
501
 
502
+ class ChartFilterRulePatch(StrictModel):
503
+ field_name: str = Field(validation_alias=AliasChoices("field_name", "fieldName", "field", "name"))
504
+ operator: ViewFilterOperator = Field(validation_alias=AliasChoices("operator", "op"))
505
+ values: list[Any] = Field(default_factory=list)
506
+
507
+ @model_validator(mode="before")
508
+ @classmethod
509
+ def normalize_aliases(cls, value: Any) -> Any:
510
+ if not isinstance(value, dict):
511
+ return value
512
+ payload = dict(value)
513
+ if "value" in payload and "values" not in payload:
514
+ payload["values"] = [payload.pop("value")]
515
+ raw_operator = payload.get("operator", payload.get("op"))
516
+ if isinstance(raw_operator, str):
517
+ normalized = raw_operator.strip().lower()
518
+ operator_aliases = {
519
+ "equals": ViewFilterOperator.eq.value,
520
+ "equal": ViewFilterOperator.eq.value,
521
+ "=": ViewFilterOperator.eq.value,
522
+ "not_equals": ViewFilterOperator.neq.value,
523
+ "not_equal": ViewFilterOperator.neq.value,
524
+ "!=": ViewFilterOperator.neq.value,
525
+ ">=": ViewFilterOperator.gte.value,
526
+ "<=": ViewFilterOperator.lte.value,
527
+ "any_of": ViewFilterOperator.in_.value,
528
+ "one_of": ViewFilterOperator.in_.value,
529
+ "between_any": ViewFilterOperator.in_.value,
530
+ "empty": ViewFilterOperator.is_empty.value,
531
+ "is blank": ViewFilterOperator.is_empty.value,
532
+ "blank": ViewFilterOperator.is_empty.value,
533
+ "not_empty": ViewFilterOperator.not_empty.value,
534
+ "not blank": ViewFilterOperator.not_empty.value,
535
+ }
536
+ if normalized in operator_aliases:
537
+ payload["operator"] = operator_aliases[normalized]
538
+ elif "operator" not in payload:
539
+ payload["operator"] = normalized
540
+ payload.pop("op", None)
541
+ return payload
542
+
543
+ @model_validator(mode="after")
544
+ def validate_shape(self) -> "ChartFilterRulePatch":
545
+ if self.operator in {ViewFilterOperator.is_empty, ViewFilterOperator.not_empty}:
546
+ self.values = []
547
+ return self
548
+ if not self.values:
549
+ raise ValueError("chart filter rule requires values")
550
+ return self
551
+
552
+
553
+ class ChartUpsertPatch(StrictModel):
554
+ chart_id: str | None = None
555
+ name: str
556
+ chart_type: PublicChartType
557
+ dimension_field_ids: list[str] = Field(default_factory=list)
558
+ indicator_field_ids: list[str] = Field(default_factory=list)
559
+ filters: list[ChartFilterRulePatch] = Field(default_factory=list)
560
+ question_config: list[dict[str, Any]] = Field(default_factory=list)
561
+ user_config: list[dict[str, Any]] = Field(default_factory=list)
562
+ config: dict[str, Any] = Field(default_factory=dict)
563
+
564
+ @model_validator(mode="before")
565
+ @classmethod
566
+ def normalize_aliases(cls, value: Any) -> Any:
567
+ if not isinstance(value, dict):
568
+ return value
569
+ payload = dict(value)
570
+ if "id" in payload and "chart_id" not in payload:
571
+ payload["chart_id"] = payload.pop("id")
572
+ if "type" in payload and "chart_type" not in payload:
573
+ payload["chart_type"] = payload.pop("type")
574
+ if "dimension_fields" in payload and "dimension_field_ids" not in payload:
575
+ payload["dimension_field_ids"] = payload.pop("dimension_fields")
576
+ if "indicator_fields" in payload and "indicator_field_ids" not in payload:
577
+ payload["indicator_field_ids"] = payload.pop("indicator_fields")
578
+ if "metric_field_ids" in payload and "indicator_field_ids" not in payload:
579
+ payload["indicator_field_ids"] = payload.pop("metric_field_ids")
580
+ raw_type = payload.get("chart_type")
581
+ if isinstance(raw_type, str):
582
+ normalized = raw_type.strip().lower()
583
+ aliases = {
584
+ "targetchart": PublicChartType.target.value,
585
+ "piechart": PublicChartType.pie.value,
586
+ "barchart": PublicChartType.bar.value,
587
+ "linechart": PublicChartType.line.value,
588
+ "tablechart": PublicChartType.table.value,
589
+ }
590
+ if normalized in aliases:
591
+ payload["chart_type"] = aliases[normalized]
592
+ if isinstance(payload.get("chart_id"), int):
593
+ payload["chart_id"] = str(payload["chart_id"])
594
+ if isinstance(payload.get("dimension_field_ids"), list):
595
+ payload["dimension_field_ids"] = [str(item) for item in payload["dimension_field_ids"] if item is not None and str(item).strip()]
596
+ if isinstance(payload.get("indicator_field_ids"), list):
597
+ payload["indicator_field_ids"] = [str(item) for item in payload["indicator_field_ids"] if item is not None and str(item).strip()]
598
+ return payload
599
+
600
+
601
+ class ChartApplyRequest(StrictModel):
602
+ app_key: str
603
+ upsert_charts: list[ChartUpsertPatch] = Field(default_factory=list)
604
+ remove_chart_ids: list[str] = Field(default_factory=list)
605
+ reorder_chart_ids: list[str] = Field(default_factory=list)
606
+
607
+ @model_validator(mode="before")
608
+ @classmethod
609
+ def normalize_ids(cls, value: Any) -> Any:
610
+ if not isinstance(value, dict):
611
+ return value
612
+ payload = dict(value)
613
+ for key in ("remove_chart_ids", "reorder_chart_ids"):
614
+ raw = payload.get(key)
615
+ if isinstance(raw, list):
616
+ payload[key] = [str(item) for item in raw if item is not None and str(item).strip()]
617
+ return payload
618
+
619
+ @model_validator(mode="after")
620
+ def validate_shape(self) -> "ChartApplyRequest":
621
+ if not self.upsert_charts and not self.remove_chart_ids and not self.reorder_chart_ids:
622
+ raise ValueError("chart apply requires at least one upsert, remove, or reorder operation")
623
+ return self
624
+
625
+
626
+ class PortalComponentPositionPatch(StrictModel):
627
+ pc_x: int = Field(default=0, validation_alias=AliasChoices("pc_x", "pcX", "x"))
628
+ pc_y: int = Field(default=0, validation_alias=AliasChoices("pc_y", "pcY", "y"))
629
+ pc_w: int = Field(default=12, validation_alias=AliasChoices("pc_w", "pcW", "w"))
630
+ pc_h: int = Field(default=8, validation_alias=AliasChoices("pc_h", "pcH", "h"))
631
+ mobile_x: int = Field(default=0, validation_alias=AliasChoices("mobile_x", "mobileX"))
632
+ mobile_y: int = Field(default=0, validation_alias=AliasChoices("mobile_y", "mobileY"))
633
+ mobile_w: int = Field(default=12, validation_alias=AliasChoices("mobile_w", "mobileW"))
634
+ mobile_h: int = Field(default=8, validation_alias=AliasChoices("mobile_h", "mobileH"))
635
+
636
+ @model_validator(mode="before")
637
+ @classmethod
638
+ def normalize_nested_layout(cls, value: Any) -> Any:
639
+ if not isinstance(value, dict):
640
+ return value
641
+ payload = dict(value)
642
+ pc = payload.pop("pc", None)
643
+ mobile = payload.pop("mobile", None)
644
+ if isinstance(pc, dict):
645
+ if "pc_x" not in payload and "x" in pc:
646
+ payload["pc_x"] = pc.get("x")
647
+ if "pc_y" not in payload and "y" in pc:
648
+ payload["pc_y"] = pc.get("y")
649
+ if "pc_w" not in payload and "cols" in pc:
650
+ payload["pc_w"] = pc.get("cols")
651
+ if "pc_h" not in payload and "rows" in pc:
652
+ payload["pc_h"] = pc.get("rows")
653
+ if isinstance(mobile, dict):
654
+ if "mobile_x" not in payload and "x" in mobile:
655
+ payload["mobile_x"] = mobile.get("x")
656
+ if "mobile_y" not in payload and "y" in mobile:
657
+ payload["mobile_y"] = mobile.get("y")
658
+ if "mobile_w" not in payload and "cols" in mobile:
659
+ payload["mobile_w"] = mobile.get("cols")
660
+ if "mobile_h" not in payload and "rows" in mobile:
661
+ payload["mobile_h"] = mobile.get("rows")
662
+ return payload
663
+
664
+
665
+ class PortalChartRefPatch(StrictModel):
666
+ app_key: str
667
+ chart_id: str | None = None
668
+ chart_name: str | None = None
669
+
670
+ @model_validator(mode="after")
671
+ def validate_target(self) -> "PortalChartRefPatch":
672
+ if not (self.chart_id or self.chart_name):
673
+ raise ValueError("chart_ref requires chart_id or chart_name")
674
+ return self
675
+
676
+
677
+ class PortalViewRefPatch(StrictModel):
678
+ app_key: str
679
+ view_key: str | None = None
680
+ view_name: str | None = None
681
+
682
+ @model_validator(mode="after")
683
+ def validate_target(self) -> "PortalViewRefPatch":
684
+ if not (self.view_key or self.view_name):
685
+ raise ValueError("view_ref requires view_key or view_name")
686
+ return self
687
+
688
+
689
+ class PortalSectionPatch(StrictModel):
690
+ title: str
691
+ source_type: str = Field(validation_alias=AliasChoices("source_type", "sourceType"))
692
+ position: PortalComponentPositionPatch | None = None
693
+ dash_style_config: dict[str, Any] | None = Field(default=None, validation_alias=AliasChoices("dash_style_config", "dashStyleConfigBO"))
694
+ config: dict[str, Any] = Field(default_factory=dict)
695
+ chart_ref: PortalChartRefPatch | None = None
696
+ view_ref: PortalViewRefPatch | None = None
697
+ text: str | None = None
698
+ url: str | None = None
699
+
700
+ @model_validator(mode="before")
701
+ @classmethod
702
+ def normalize_aliases(cls, value: Any) -> Any:
703
+ if not isinstance(value, dict):
704
+ return value
705
+ payload = dict(value)
706
+ raw_type = payload.get("source_type", payload.get("sourceType"))
707
+ if isinstance(raw_type, str):
708
+ payload["source_type"] = raw_type.strip().lower()
709
+ if "chartRef" in payload and "chart_ref" not in payload:
710
+ payload["chart_ref"] = payload.pop("chartRef")
711
+ if "viewRef" in payload and "view_ref" not in payload:
712
+ payload["view_ref"] = payload.pop("viewRef")
713
+ if "dashStyleConfigBO" in payload and "dash_style_config" not in payload:
714
+ payload["dash_style_config"] = payload.pop("dashStyleConfigBO")
715
+ return payload
716
+
717
+ @model_validator(mode="after")
718
+ def validate_shape(self) -> "PortalSectionPatch":
719
+ supported = {"chart", "view", "grid", "filter", "text", "link"}
720
+ if self.source_type not in supported:
721
+ raise ValueError(f"unsupported portal source_type '{self.source_type}'")
722
+ if self.source_type == "chart" and self.chart_ref is None:
723
+ raise ValueError("chart section requires chart_ref")
724
+ if self.source_type == "view" and self.view_ref is None:
725
+ raise ValueError("view section requires view_ref")
726
+ if self.source_type == "text" and self.text is None:
727
+ raise ValueError("text section requires text")
728
+ if self.source_type == "link" and self.url is None:
729
+ raise ValueError("link section requires url")
730
+ return self
731
+
732
+
733
+ class PortalApplyRequest(StrictModel):
734
+ dash_key: str | None = None
735
+ dash_name: str | None = None
736
+ package_tag_id: int | None = None
737
+ publish: bool = True
738
+ sections: list[PortalSectionPatch] = Field(default_factory=list)
739
+ auth: dict[str, Any] | None = None
740
+ icon: str | None = None
741
+ color: str | None = None
742
+ hide_copyright: bool | None = Field(default=None, validation_alias=AliasChoices("hide_copyright", "hideCopyright"))
743
+ dash_global_config: dict[str, Any] | None = Field(default=None, validation_alias=AliasChoices("dash_global_config", "dashGlobalConfig"))
744
+ config: dict[str, Any] = Field(default_factory=dict)
745
+
746
+ @model_validator(mode="after")
747
+ def validate_shape(self) -> "PortalApplyRequest":
748
+ if not self.dash_key and not self.package_tag_id:
749
+ raise ValueError("package_tag_id is required when dash_key is empty")
750
+ if not self.dash_key and not self.dash_name:
751
+ raise ValueError("dash_name is required when creating a portal")
752
+ if not self.sections:
753
+ raise ValueError("portal apply requires a non-empty sections list")
754
+ return self
755
+
756
+
494
757
  FieldPatch.model_rebuild()
495
758
 
496
759