@josephyan/qingflow-app-user-mcp 0.1.0-beta.9 → 0.2.0-beta.2

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.1.0-beta.9
6
+ npm install @josephyan/qingflow-app-user-mcp@0.2.0-beta.2
7
7
  ```
8
8
 
9
9
  Run:
10
10
 
11
11
  ```bash
12
- npx -y -p @josephyan/qingflow-app-user-mcp@0.1.0-beta.9 qingflow-app-user-mcp
12
+ npx -y -p @josephyan/qingflow-app-user-mcp@0.2.0-beta.2 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.1.0-beta.9",
3
+ "version": "0.2.0-beta.2",
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.1.0b9"
7
+ version = "0.2.0b2"
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.1.0b9"
5
+ __version__ = "0.2.0b2"
@@ -0,0 +1,3 @@
1
+ from .service import AiBuilderFacade
2
+
3
+ __all__ = ["AiBuilderFacade"]
@@ -0,0 +1,346 @@
1
+ from __future__ import annotations
2
+
3
+ from enum import Enum
4
+ from typing import Any
5
+
6
+ from pydantic import AliasChoices, Field, model_validator
7
+
8
+ from ..solution.spec_models import StrictModel
9
+
10
+
11
+ class PublicFieldType(str, Enum):
12
+ text = "text"
13
+ long_text = "long_text"
14
+ number = "number"
15
+ amount = "amount"
16
+ date = "date"
17
+ datetime = "datetime"
18
+ member = "member"
19
+ department = "department"
20
+ single_select = "single_select"
21
+ multi_select = "multi_select"
22
+ phone = "phone"
23
+ email = "email"
24
+ address = "address"
25
+ attachment = "attachment"
26
+ boolean = "boolean"
27
+ relation = "relation"
28
+ subtable = "subtable"
29
+
30
+
31
+ FIELD_TYPE_ALIASES: dict[str, PublicFieldType] = {
32
+ "textarea": PublicFieldType.long_text,
33
+ "amount": PublicFieldType.amount,
34
+ "currency": PublicFieldType.amount,
35
+ "mobile": PublicFieldType.phone,
36
+ "user": PublicFieldType.member,
37
+ "users": PublicFieldType.member,
38
+ "select": PublicFieldType.single_select,
39
+ "radio": PublicFieldType.single_select,
40
+ "checkbox": PublicFieldType.multi_select,
41
+ "multi_select": PublicFieldType.multi_select,
42
+ "multi-select": PublicFieldType.multi_select,
43
+ "departments": PublicFieldType.department,
44
+ }
45
+
46
+
47
+ class PublicViewType(str, Enum):
48
+ table = "table"
49
+ card = "card"
50
+ board = "board"
51
+
52
+
53
+ class LayoutApplyMode(str, Enum):
54
+ merge = "merge"
55
+ replace = "replace"
56
+
57
+
58
+ class LayoutPreset(str, Enum):
59
+ balanced = "balanced"
60
+ compact = "compact"
61
+ single_section = "single_section"
62
+
63
+
64
+ class FlowPreset(str, Enum):
65
+ basic_approval = "basic_approval"
66
+ basic_fill_then_approve = "basic_fill_then_approve"
67
+
68
+
69
+ class ViewsPreset(str, Enum):
70
+ default_table = "default_table"
71
+ status_board = "status_board"
72
+
73
+
74
+ class PublicFlowNodeType(str, Enum):
75
+ start = "start"
76
+ approve = "approve"
77
+ fill = "fill"
78
+ copy = "copy"
79
+ branch = "branch"
80
+ condition = "condition"
81
+ webhook = "webhook"
82
+ end = "end"
83
+
84
+
85
+ class FieldSelector(StrictModel):
86
+ field_id: str | None = None
87
+ que_id: int | None = None
88
+ name: str | None = Field(default=None, validation_alias=AliasChoices("name", "title", "label"))
89
+
90
+ @model_validator(mode="after")
91
+ def validate_selector(self) -> "FieldSelector":
92
+ if not any((self.field_id, self.que_id, self.name)):
93
+ raise ValueError("selector must include field_id, que_id, or name")
94
+ return self
95
+
96
+
97
+ class FieldPatch(StrictModel):
98
+ name: str = Field(validation_alias=AliasChoices("name", "title", "label"))
99
+ type: PublicFieldType
100
+ required: bool = False
101
+ description: str | None = None
102
+ options: list[str] = Field(default_factory=list)
103
+ target_app_key: str | None = None
104
+ subfields: list["FieldPatch"] = Field(default_factory=list)
105
+
106
+ @model_validator(mode="after")
107
+ def validate_shape(self) -> "FieldPatch":
108
+ if self.type == PublicFieldType.relation and not self.target_app_key:
109
+ raise ValueError("relation field requires target_app_key")
110
+ if self.type != PublicFieldType.relation and self.target_app_key:
111
+ raise ValueError("target_app_key is only allowed for relation fields")
112
+ if self.type == PublicFieldType.subtable and not self.subfields:
113
+ raise ValueError("subtable field requires subfields")
114
+ if self.type != PublicFieldType.subtable and self.subfields:
115
+ raise ValueError("subfields are only allowed for subtable fields")
116
+ return self
117
+
118
+ @model_validator(mode="before")
119
+ @classmethod
120
+ def normalize_aliases(cls, value: Any) -> Any:
121
+ return _normalize_field_payload(value)
122
+
123
+
124
+ class FieldMutation(StrictModel):
125
+ name: str | None = Field(default=None, validation_alias=AliasChoices("name", "title", "label"))
126
+ type: PublicFieldType | None = None
127
+ required: bool | None = None
128
+ description: str | None = None
129
+ options: list[str] | None = None
130
+ target_app_key: str | None = None
131
+ subfields: list[FieldPatch] | None = None
132
+
133
+ @model_validator(mode="after")
134
+ def validate_shape(self) -> "FieldMutation":
135
+ if self.type == PublicFieldType.relation and not self.target_app_key:
136
+ raise ValueError("relation field requires target_app_key")
137
+ if self.type == PublicFieldType.subtable and not self.subfields:
138
+ raise ValueError("subtable field requires subfields")
139
+ return self
140
+
141
+ @model_validator(mode="before")
142
+ @classmethod
143
+ def normalize_aliases(cls, value: Any) -> Any:
144
+ return _normalize_field_payload(value)
145
+
146
+
147
+ class FieldUpdatePatch(StrictModel):
148
+ selector: FieldSelector
149
+ set: FieldMutation
150
+
151
+
152
+ class FieldRemovePatch(StrictModel):
153
+ field_id: str | None = None
154
+ que_id: int | None = None
155
+ name: str | None = Field(default=None, validation_alias=AliasChoices("name", "title", "label"))
156
+
157
+ @model_validator(mode="after")
158
+ def validate_shape(self) -> "FieldRemovePatch":
159
+ if not any((self.field_id, self.que_id, self.name)):
160
+ raise ValueError("remove patch must include field_id, que_id, or name")
161
+ return self
162
+
163
+
164
+ class LayoutSectionPatch(StrictModel):
165
+ section_id: str | None = Field(default=None, validation_alias=AliasChoices("section_id", "sectionId"))
166
+ title: str
167
+ rows: list[list[str]] = Field(default_factory=list)
168
+
169
+ @model_validator(mode="after")
170
+ def validate_rows(self) -> "LayoutSectionPatch":
171
+ if not self.rows:
172
+ raise ValueError("section rows must be a non-empty list")
173
+ if not self.section_id:
174
+ self.section_id = _slugify_title(self.title)
175
+ return self
176
+
177
+
178
+ class FlowNodePatch(StrictModel):
179
+ id: str
180
+ type: PublicFlowNodeType
181
+ name: str
182
+ config: dict[str, Any] = Field(default_factory=dict)
183
+
184
+
185
+ class FlowTransitionPatch(StrictModel):
186
+ source: str = Field(alias="from")
187
+ target: str = Field(alias="to")
188
+
189
+
190
+ class ViewUpsertPatch(StrictModel):
191
+ name: str
192
+ type: PublicViewType
193
+ columns: list[str] = Field(default_factory=list)
194
+ group_by: str | None = None
195
+
196
+ @model_validator(mode="before")
197
+ @classmethod
198
+ def normalize_aliases(cls, value: Any) -> Any:
199
+ if not isinstance(value, dict):
200
+ return value
201
+ payload = dict(value)
202
+ if "fields" in payload and "columns" not in payload:
203
+ payload["columns"] = payload.pop("fields")
204
+ raw_type = payload.get("type")
205
+ if isinstance(raw_type, str):
206
+ normalized = raw_type.strip().lower()
207
+ if normalized == "tableview":
208
+ payload["type"] = "table"
209
+ elif normalized == "cardview":
210
+ payload["type"] = "card"
211
+ elif normalized == "kanban":
212
+ payload["type"] = "board"
213
+ return payload
214
+
215
+ @model_validator(mode="after")
216
+ def validate_shape(self) -> "ViewUpsertPatch":
217
+ if self.type in {PublicViewType.table, PublicViewType.card} and not self.columns:
218
+ raise ValueError("table/card views require columns")
219
+ if self.type == PublicViewType.board and not self.group_by:
220
+ raise ValueError("board view requires group_by")
221
+ return self
222
+
223
+
224
+ FieldPatch.model_rebuild()
225
+
226
+
227
+ class AppReadSummaryResponse(StrictModel):
228
+ app_key: str
229
+ title: str | None = None
230
+ tag_ids: list[int] = Field(default_factory=list)
231
+ publish_status: int | None = None
232
+ field_count: int = 0
233
+ layout_section_count: int = 0
234
+ view_count: int = 0
235
+ workflow_enabled: bool = False
236
+ verification_hints: list[str] = Field(default_factory=list)
237
+
238
+
239
+ class AppFieldsReadResponse(StrictModel):
240
+ app_key: str
241
+ fields: list[dict[str, Any]] = Field(default_factory=list)
242
+ field_count: int = 0
243
+
244
+
245
+ class AppLayoutReadResponse(StrictModel):
246
+ app_key: str
247
+ sections: list[dict[str, Any]] = Field(default_factory=list)
248
+ unplaced_fields: list[str] = Field(default_factory=list)
249
+ layout_mode_detected: str = "empty"
250
+
251
+
252
+ class AppViewsReadResponse(StrictModel):
253
+ app_key: str
254
+ views: list[dict[str, Any]] = Field(default_factory=list)
255
+
256
+
257
+ class AppFlowReadResponse(StrictModel):
258
+ app_key: str
259
+ enabled: bool = False
260
+ nodes: list[dict[str, Any]] = Field(default_factory=list)
261
+ transitions: list[dict[str, Any]] = Field(default_factory=list)
262
+
263
+
264
+ class SchemaPlanRequest(StrictModel):
265
+ app_key: str = ""
266
+ package_tag_id: int | None = None
267
+ app_name: str = Field(default="", validation_alias=AliasChoices("app_name", "app_title", "title"))
268
+ create_if_missing: bool = False
269
+ add_fields: list[FieldPatch] = Field(default_factory=list)
270
+ update_fields: list[FieldUpdatePatch] = Field(default_factory=list)
271
+ remove_fields: list[FieldRemovePatch] = Field(default_factory=list)
272
+
273
+
274
+ class LayoutPlanRequest(StrictModel):
275
+ app_key: str
276
+ mode: LayoutApplyMode = LayoutApplyMode.merge
277
+ sections: list[LayoutSectionPatch] = Field(default_factory=list)
278
+ preset: LayoutPreset | None = None
279
+
280
+ @model_validator(mode="before")
281
+ @classmethod
282
+ def normalize_mode_alias(cls, value: Any) -> Any:
283
+ if not isinstance(value, dict):
284
+ return value
285
+ payload = dict(value)
286
+ if str(payload.get("mode") or "").strip().lower() == "overwrite":
287
+ payload["mode"] = "replace"
288
+ return payload
289
+
290
+
291
+ class FlowPlanRequest(StrictModel):
292
+ app_key: str
293
+ mode: str = "replace"
294
+ nodes: list[FlowNodePatch] = Field(default_factory=list)
295
+ transitions: list[FlowTransitionPatch] = Field(default_factory=list)
296
+ preset: FlowPreset | None = None
297
+
298
+ @model_validator(mode="before")
299
+ @classmethod
300
+ def normalize_mode_alias(cls, value: Any) -> Any:
301
+ if not isinstance(value, dict):
302
+ return value
303
+ payload = dict(value)
304
+ if str(payload.get("mode") or "").strip().lower() == "overwrite":
305
+ payload["mode"] = "replace"
306
+ return payload
307
+
308
+
309
+ class ViewsPlanRequest(StrictModel):
310
+ app_key: str
311
+ upsert_views: list[ViewUpsertPatch] = Field(default_factory=list)
312
+ remove_views: list[str] = Field(default_factory=list)
313
+ preset: ViewsPreset | None = None
314
+
315
+
316
+ class OperationResultEnvelope(StrictModel):
317
+ status: str
318
+ error_code: str | None = None
319
+ recoverable: bool = False
320
+ message: str
321
+ normalized_args: dict[str, Any] = Field(default_factory=dict)
322
+ missing_fields: list[str] = Field(default_factory=list)
323
+ allowed_values: dict[str, Any] = Field(default_factory=dict)
324
+ details: dict[str, Any] = Field(default_factory=dict)
325
+ request_id: str | None = None
326
+ suggested_next_call: dict[str, Any] | None = None
327
+ noop: bool = False
328
+ verification: dict[str, Any] = Field(default_factory=dict)
329
+
330
+
331
+ def _normalize_field_payload(value: Any) -> Any:
332
+ if not isinstance(value, dict):
333
+ return value
334
+ payload = dict(value)
335
+ raw_type = payload.get("type")
336
+ if isinstance(raw_type, str):
337
+ normalized = FIELD_TYPE_ALIASES.get(raw_type.strip().lower())
338
+ if normalized is not None:
339
+ payload["type"] = normalized.value
340
+ return payload
341
+
342
+
343
+ def _slugify_title(title: str) -> str:
344
+ normalized = "".join(ch.lower() if ch.isalnum() else "_" for ch in str(title or ""))
345
+ collapsed = "_".join(part for part in normalized.split("_") if part)
346
+ return collapsed or "section"