@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 +2 -2
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/src/qingflow_mcp/__init__.py +1 -1
- package/src/qingflow_mcp/builder_facade/__init__.py +3 -0
- package/src/qingflow_mcp/builder_facade/models.py +346 -0
- package/src/qingflow_mcp/builder_facade/service.py +2770 -0
- package/src/qingflow_mcp/server_app_builder.py +139 -171
- package/src/qingflow_mcp/solution/build_assembly_store.py +47 -3
- package/src/qingflow_mcp/solution/compiler/__init__.py +19 -2
- package/src/qingflow_mcp/solution/executor.py +244 -7
- package/src/qingflow_mcp/solution/requirements_builder.py +536 -0
- package/src/qingflow_mcp/solution/run_store.py +33 -10
- package/src/qingflow_mcp/tools/ai_builder_tools.py +577 -0
- package/src/qingflow_mcp/tools/app_tools.py +72 -5
- package/src/qingflow_mcp/tools/auth_tools.py +46 -1
- package/src/qingflow_mcp/tools/file_tools.py +9 -0
- package/src/qingflow_mcp/tools/package_tools.py +97 -12
- package/src/qingflow_mcp/tools/solution_tools.py +1234 -27
- package/src/qingflow_mcp/tools/workspace_tools.py +24 -2
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.
|
|
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.
|
|
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
package/pyproject.toml
CHANGED
|
@@ -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"
|