@josephyan/qingflow-cli 0.2.0-beta.69 → 0.2.0-beta.70
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/backend_client.py +0 -1
- package/src/qingflow_mcp/builder_facade/models.py +34 -0
- package/src/qingflow_mcp/builder_facade/service.py +337 -17
- package/src/qingflow_mcp/cli/commands/builder.py +248 -1
- package/src/qingflow_mcp/cli/commands/common.py +15 -0
- package/src/qingflow_mcp/cli/commands/imports.py +12 -2
- package/src/qingflow_mcp/cli/commands/record.py +132 -32
- package/src/qingflow_mcp/cli/commands/workspace.py +1 -1
- package/src/qingflow_mcp/cli/formatters.py +52 -2
- package/src/qingflow_mcp/cli/main.py +7 -5
- package/src/qingflow_mcp/response_trim.py +668 -0
- package/src/qingflow_mcp/server_app_builder.py +136 -8
- package/src/qingflow_mcp/server_app_user.py +55 -11
- package/src/qingflow_mcp/tools/ai_builder_tools.py +270 -5
- package/src/qingflow_mcp/tools/app_tools.py +29 -0
- package/src/qingflow_mcp/tools/auth_tools.py +259 -1
- package/src/qingflow_mcp/tools/import_tools.py +59 -7
- package/src/qingflow_mcp/tools/qingbi_report_tools.py +23 -6
- package/src/qingflow_mcp/tools/record_tools.py +6 -12
- package/src/qingflow_mcp/tools/workspace_tools.py +124 -7
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import json
|
|
4
|
+
|
|
3
5
|
from mcp.server.fastmcp import FastMCP
|
|
4
6
|
|
|
5
7
|
from .backend_client import BackendClient
|
|
6
8
|
from .config import DEFAULT_PROFILE
|
|
9
|
+
from .response_trim import BUILDER_SERVER_METHOD_MAP, trim_error_response, trim_public_response, wrap_trimmed_methods
|
|
7
10
|
from .session_store import SessionStore
|
|
8
11
|
from .tools.ai_builder_tools import AiBuilderTools
|
|
9
12
|
from .tools.auth_tools import AuthTools
|
|
@@ -12,6 +15,17 @@ from .tools.file_tools import FileTools
|
|
|
12
15
|
from .tools.workspace_tools import WorkspaceTools
|
|
13
16
|
|
|
14
17
|
|
|
18
|
+
def _config_failure(message: str, *, fix_hint: str) -> dict:
|
|
19
|
+
return {
|
|
20
|
+
"status": "failed",
|
|
21
|
+
"error_code": "CONFIG_ERROR",
|
|
22
|
+
"recoverable": True,
|
|
23
|
+
"message": message,
|
|
24
|
+
"details": {"fix_hint": fix_hint},
|
|
25
|
+
"verification": {},
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
15
29
|
def build_builder_server() -> FastMCP:
|
|
16
30
|
server = FastMCP(
|
|
17
31
|
"Qingflow App Builder MCP",
|
|
@@ -21,7 +35,7 @@ def build_builder_server() -> FastMCP:
|
|
|
21
35
|
"Follow the resource path resolve -> summary read -> apply -> attach -> publish_verify. "
|
|
22
36
|
"Use builder_tool_contract when you need a machine-readable contract, aliases, allowed enums, or a minimal valid example for a public builder tool. "
|
|
23
37
|
"If creating a new package may be appropriate, ask the user to confirm package creation before calling package_create; otherwise use package_resolve/package_list and app_resolve to locate resources, "
|
|
24
|
-
"app_read_summary/app_read_fields/app_read_layout_summary/app_read_views_summary/app_read_flow_summary/app_read_charts_summary/portal_read_summary for
|
|
38
|
+
"app_read_summary/app_read_fields/app_read_layout_summary/app_read_views_summary/app_read_flow_summary/app_read_charts_summary/portal_list/portal_get/portal_read_summary/view_get/chart_get for reads, "
|
|
25
39
|
"member_search/role_search/role_create when workflow assignees must come from the directory or role catalog, preferring roles over explicit members unless the user explicitly names members, "
|
|
26
40
|
"then app_schema_apply/app_layout_apply/app_flow_apply/app_views_apply/app_charts_apply/portal_apply to execute normalized patches; these apply tools perform planning, normalization, and dependency checks internally where applicable. Schema/layout/views noop requests skip publish, charts are immediate-live without publish and resolve targets by chart_id first then exact unique chart name, portal updates are replace-only and publish=false only guarantees draft/base-info updates, and flow should use publish=false whenever you only want draft/precheck behavior. "
|
|
27
41
|
"Use package_attach_app to attach apps to packages, and app_publish_verify for explicit final publish verification. "
|
|
@@ -33,10 +47,10 @@ def build_builder_server() -> FastMCP:
|
|
|
33
47
|
)
|
|
34
48
|
sessions = SessionStore()
|
|
35
49
|
backend = BackendClient()
|
|
36
|
-
auth = AuthTools(sessions, backend)
|
|
37
|
-
workspace = WorkspaceTools(sessions, backend)
|
|
38
|
-
files = FileTools(sessions, backend)
|
|
39
|
-
ai_builder = AiBuilderTools(sessions, backend)
|
|
50
|
+
auth = wrap_trimmed_methods(AuthTools(sessions, backend), BUILDER_SERVER_METHOD_MAP)
|
|
51
|
+
workspace = wrap_trimmed_methods(WorkspaceTools(sessions, backend), BUILDER_SERVER_METHOD_MAP)
|
|
52
|
+
files = wrap_trimmed_methods(FileTools(sessions, backend), BUILDER_SERVER_METHOD_MAP)
|
|
53
|
+
ai_builder = wrap_trimmed_methods(AiBuilderTools(sessions, backend), BUILDER_SERVER_METHOD_MAP)
|
|
40
54
|
feedback = FeedbackTools(backend, mcp_side="App Builder MCP")
|
|
41
55
|
|
|
42
56
|
@server.tool()
|
|
@@ -123,7 +137,45 @@ def build_builder_server() -> FastMCP:
|
|
|
123
137
|
file_related_url=file_related_url,
|
|
124
138
|
)
|
|
125
139
|
|
|
126
|
-
|
|
140
|
+
@server.tool()
|
|
141
|
+
def feedback_submit(
|
|
142
|
+
category: str = "",
|
|
143
|
+
title: str = "",
|
|
144
|
+
description: str = "",
|
|
145
|
+
expected_behavior: str | None = None,
|
|
146
|
+
actual_behavior: str | None = None,
|
|
147
|
+
impact_scope: str | None = None,
|
|
148
|
+
tool_name: str | None = None,
|
|
149
|
+
app_key: str | None = None,
|
|
150
|
+
record_id: str | int | None = None,
|
|
151
|
+
workflow_node_id: str | int | None = None,
|
|
152
|
+
note: str | None = None,
|
|
153
|
+
) -> dict:
|
|
154
|
+
try:
|
|
155
|
+
return trim_public_response(
|
|
156
|
+
"feedback_submit",
|
|
157
|
+
feedback.feedback_submit(
|
|
158
|
+
category=category,
|
|
159
|
+
title=title,
|
|
160
|
+
description=description,
|
|
161
|
+
expected_behavior=expected_behavior,
|
|
162
|
+
actual_behavior=actual_behavior,
|
|
163
|
+
impact_scope=impact_scope,
|
|
164
|
+
tool_name=tool_name,
|
|
165
|
+
app_key=app_key,
|
|
166
|
+
record_id=record_id,
|
|
167
|
+
workflow_node_id=workflow_node_id,
|
|
168
|
+
note=note,
|
|
169
|
+
),
|
|
170
|
+
)
|
|
171
|
+
except RuntimeError as exc:
|
|
172
|
+
try:
|
|
173
|
+
payload = json.loads(str(exc))
|
|
174
|
+
except json.JSONDecodeError:
|
|
175
|
+
raise
|
|
176
|
+
if isinstance(payload, dict):
|
|
177
|
+
raise RuntimeError(json.dumps(trim_error_response(payload), ensure_ascii=False)) from None
|
|
178
|
+
raise
|
|
127
179
|
|
|
128
180
|
@server.tool()
|
|
129
181
|
def package_list(profile: str = DEFAULT_PROFILE, trial_status: str = "all") -> dict:
|
|
@@ -194,9 +246,8 @@ def build_builder_server() -> FastMCP:
|
|
|
194
246
|
profile: str = DEFAULT_PROFILE,
|
|
195
247
|
tag_id: int = 0,
|
|
196
248
|
app_key: str = "",
|
|
197
|
-
app_title: str = "",
|
|
198
249
|
) -> dict:
|
|
199
|
-
return ai_builder.package_attach_app(profile=profile, tag_id=tag_id, app_key=app_key
|
|
250
|
+
return ai_builder.package_attach_app(profile=profile, tag_id=tag_id, app_key=app_key)
|
|
200
251
|
|
|
201
252
|
@server.tool()
|
|
202
253
|
def app_release_edit_lock_if_mine(
|
|
@@ -219,6 +270,19 @@ def build_builder_server() -> FastMCP:
|
|
|
219
270
|
app_name: str = "",
|
|
220
271
|
package_tag_id: int | None = None,
|
|
221
272
|
) -> dict:
|
|
273
|
+
has_app_key = bool((app_key or "").strip())
|
|
274
|
+
has_app_name = bool((app_name or "").strip())
|
|
275
|
+
has_package_tag_id = package_tag_id is not None
|
|
276
|
+
if has_app_key and (has_app_name or has_package_tag_id):
|
|
277
|
+
return _config_failure(
|
|
278
|
+
"app_resolve accepts exactly one selector mode.",
|
|
279
|
+
fix_hint="Use only `app_key`, or use `app_name` together with `package_tag_id`.",
|
|
280
|
+
)
|
|
281
|
+
if not has_app_key and not (has_app_name and has_package_tag_id):
|
|
282
|
+
return _config_failure(
|
|
283
|
+
"app_resolve requires either app_key, or app_name together with package_tag_id.",
|
|
284
|
+
fix_hint="For an existing known app, pass `app_key`. For package-scoped lookup, pass both `app_name` and `package_tag_id`.",
|
|
285
|
+
)
|
|
222
286
|
return ai_builder.app_resolve(profile=profile, app_key=app_key, app_name=app_name, package_tag_id=package_tag_id)
|
|
223
287
|
|
|
224
288
|
@server.tool()
|
|
@@ -270,6 +334,18 @@ def build_builder_server() -> FastMCP:
|
|
|
270
334
|
def app_read_charts_summary(profile: str = DEFAULT_PROFILE, app_key: str = "") -> dict:
|
|
271
335
|
return ai_builder.app_read_charts_summary(profile=profile, app_key=app_key)
|
|
272
336
|
|
|
337
|
+
@server.tool()
|
|
338
|
+
def portal_list(profile: str = DEFAULT_PROFILE) -> dict:
|
|
339
|
+
return ai_builder.portal_list(profile=profile)
|
|
340
|
+
|
|
341
|
+
@server.tool()
|
|
342
|
+
def portal_get(
|
|
343
|
+
profile: str = DEFAULT_PROFILE,
|
|
344
|
+
dash_key: str = "",
|
|
345
|
+
being_draft: bool = True,
|
|
346
|
+
) -> dict:
|
|
347
|
+
return ai_builder.portal_get(profile=profile, dash_key=dash_key, being_draft=being_draft)
|
|
348
|
+
|
|
273
349
|
@server.tool()
|
|
274
350
|
def portal_read_summary(
|
|
275
351
|
profile: str = DEFAULT_PROFILE,
|
|
@@ -278,6 +354,30 @@ def build_builder_server() -> FastMCP:
|
|
|
278
354
|
) -> dict:
|
|
279
355
|
return ai_builder.portal_read_summary(profile=profile, dash_key=dash_key, being_draft=being_draft)
|
|
280
356
|
|
|
357
|
+
@server.tool()
|
|
358
|
+
def view_get(profile: str = DEFAULT_PROFILE, viewgraph_key: str = "") -> dict:
|
|
359
|
+
return ai_builder.view_get(profile=profile, viewgraph_key=viewgraph_key)
|
|
360
|
+
|
|
361
|
+
@server.tool()
|
|
362
|
+
def chart_get(
|
|
363
|
+
profile: str = DEFAULT_PROFILE,
|
|
364
|
+
chart_id: str = "",
|
|
365
|
+
data_payload: dict | None = None,
|
|
366
|
+
page_num: int | None = None,
|
|
367
|
+
page_size: int | None = None,
|
|
368
|
+
page_num_y: int | None = None,
|
|
369
|
+
page_size_y: int | None = None,
|
|
370
|
+
) -> dict:
|
|
371
|
+
return ai_builder.chart_get(
|
|
372
|
+
profile=profile,
|
|
373
|
+
chart_id=chart_id,
|
|
374
|
+
data_payload=data_payload or {},
|
|
375
|
+
page_num=page_num,
|
|
376
|
+
page_size=page_size,
|
|
377
|
+
page_num_y=page_num_y,
|
|
378
|
+
page_size_y=page_size_y,
|
|
379
|
+
)
|
|
380
|
+
|
|
281
381
|
@server.tool()
|
|
282
382
|
def app_schema_apply(
|
|
283
383
|
profile: str = DEFAULT_PROFILE,
|
|
@@ -293,6 +393,21 @@ def build_builder_server() -> FastMCP:
|
|
|
293
393
|
update_fields: list[dict] | None = None,
|
|
294
394
|
remove_fields: list[dict] | None = None,
|
|
295
395
|
) -> dict:
|
|
396
|
+
has_app_key = bool((app_key or "").strip())
|
|
397
|
+
has_app_name = bool((app_name or "").strip())
|
|
398
|
+
has_app_title = bool((app_title or "").strip())
|
|
399
|
+
has_package_tag_id = package_tag_id is not None
|
|
400
|
+
if has_app_key:
|
|
401
|
+
if create_if_missing or has_app_name or has_package_tag_id or has_app_title:
|
|
402
|
+
return _config_failure(
|
|
403
|
+
"app_schema_apply edit mode only accepts app_key as the resource selector.",
|
|
404
|
+
fix_hint="For existing apps, pass `app_key` only. For create mode, use `package_tag_id + app_name + create_if_missing=true`.",
|
|
405
|
+
)
|
|
406
|
+
elif not (create_if_missing and has_package_tag_id and has_app_name):
|
|
407
|
+
return _config_failure(
|
|
408
|
+
"app_schema_apply create mode requires package_tag_id, app_name, and create_if_missing=true.",
|
|
409
|
+
fix_hint="Use `app_key` for existing apps, or pass `package_tag_id + app_name + create_if_missing=true` to create a new app.",
|
|
410
|
+
)
|
|
296
411
|
return ai_builder.app_schema_apply(
|
|
297
412
|
profile=profile,
|
|
298
413
|
app_key=app_key,
|
|
@@ -383,6 +498,19 @@ def build_builder_server() -> FastMCP:
|
|
|
383
498
|
dash_global_config: dict | None = None,
|
|
384
499
|
config: dict | None = None,
|
|
385
500
|
) -> dict:
|
|
501
|
+
has_dash_key = bool((dash_key or "").strip())
|
|
502
|
+
has_dash_name = bool((dash_name or "").strip())
|
|
503
|
+
has_package_tag_id = package_tag_id is not None
|
|
504
|
+
if has_dash_key and has_package_tag_id:
|
|
505
|
+
return _config_failure(
|
|
506
|
+
"portal_apply accepts exactly one selector mode.",
|
|
507
|
+
fix_hint="Use `dash_key` to update an existing portal, or use `package_tag_id + dash_name` to create a new portal.",
|
|
508
|
+
)
|
|
509
|
+
if not has_dash_key and not (has_package_tag_id and has_dash_name):
|
|
510
|
+
return _config_failure(
|
|
511
|
+
"portal_apply requires either dash_key, or package_tag_id together with dash_name.",
|
|
512
|
+
fix_hint="Use `dash_key` for an existing portal. For create mode, pass `package_tag_id + dash_name`.",
|
|
513
|
+
)
|
|
386
514
|
return ai_builder.portal_apply(
|
|
387
515
|
profile=profile,
|
|
388
516
|
dash_key=dash_key,
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import json
|
|
3
4
|
from datetime import date
|
|
4
5
|
|
|
5
6
|
from mcp.server.fastmcp import FastMCP
|
|
6
7
|
|
|
7
8
|
from .backend_client import BackendClient
|
|
8
9
|
from .config import DEFAULT_PROFILE
|
|
10
|
+
from .response_trim import USER_SERVER_METHOD_MAP, trim_error_response, trim_public_response, wrap_trimmed_methods
|
|
9
11
|
from .session_store import SessionStore
|
|
10
12
|
from .tools.app_tools import AppTools
|
|
11
13
|
from .tools.auth_tools import AuthTools
|
|
@@ -170,12 +172,15 @@ If the current MCP capability is unsupported, the workflow is awkward, or the us
|
|
|
170
172
|
)
|
|
171
173
|
sessions = SessionStore()
|
|
172
174
|
backend = BackendClient()
|
|
173
|
-
auth = AuthTools(sessions, backend)
|
|
174
|
-
apps = AppTools(sessions, backend)
|
|
175
|
-
workspace = WorkspaceTools(sessions, backend)
|
|
176
|
-
|
|
177
|
-
imports = ImportTools(sessions, backend)
|
|
175
|
+
auth = wrap_trimmed_methods(AuthTools(sessions, backend), USER_SERVER_METHOD_MAP)
|
|
176
|
+
apps = wrap_trimmed_methods(AppTools(sessions, backend), USER_SERVER_METHOD_MAP)
|
|
177
|
+
workspace = wrap_trimmed_methods(WorkspaceTools(sessions, backend), USER_SERVER_METHOD_MAP)
|
|
178
|
+
file_tools = wrap_trimmed_methods(FileTools(sessions, backend), USER_SERVER_METHOD_MAP)
|
|
179
|
+
imports = wrap_trimmed_methods(ImportTools(sessions, backend), USER_SERVER_METHOD_MAP)
|
|
178
180
|
feedback = FeedbackTools(backend, mcp_side="App User MCP")
|
|
181
|
+
code_block_tools = wrap_trimmed_methods(CodeBlockTools(sessions, backend), USER_SERVER_METHOD_MAP)
|
|
182
|
+
task_context_tools = wrap_trimmed_methods(TaskContextTools(sessions, backend), USER_SERVER_METHOD_MAP)
|
|
183
|
+
directory_tools = wrap_trimmed_methods(DirectoryTools(sessions, backend), USER_SERVER_METHOD_MAP)
|
|
179
184
|
|
|
180
185
|
@server.tool()
|
|
181
186
|
def auth_login(
|
|
@@ -263,7 +268,7 @@ If the current MCP capability is unsupported, the workflow is awkward, or the us
|
|
|
263
268
|
path_id: int | None = None,
|
|
264
269
|
file_related_url: str | None = None,
|
|
265
270
|
) -> dict:
|
|
266
|
-
return
|
|
271
|
+
return file_tools.file_get_upload_info(
|
|
267
272
|
profile=profile,
|
|
268
273
|
upload_kind=upload_kind,
|
|
269
274
|
file_name=file_name,
|
|
@@ -286,7 +291,7 @@ If the current MCP capability is unsupported, the workflow is awkward, or the us
|
|
|
286
291
|
path_id: int | None = None,
|
|
287
292
|
file_related_url: str | None = None,
|
|
288
293
|
) -> dict:
|
|
289
|
-
return
|
|
294
|
+
return file_tools.file_upload_local(
|
|
290
295
|
profile=profile,
|
|
291
296
|
upload_kind=upload_kind,
|
|
292
297
|
file_path=file_path,
|
|
@@ -299,10 +304,49 @@ If the current MCP capability is unsupported, the workflow is awkward, or the us
|
|
|
299
304
|
|
|
300
305
|
imports.register(server)
|
|
301
306
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
307
|
+
@server.tool()
|
|
308
|
+
def feedback_submit(
|
|
309
|
+
category: str = "",
|
|
310
|
+
title: str = "",
|
|
311
|
+
description: str = "",
|
|
312
|
+
expected_behavior: str | None = None,
|
|
313
|
+
actual_behavior: str | None = None,
|
|
314
|
+
impact_scope: str | None = None,
|
|
315
|
+
tool_name: str | None = None,
|
|
316
|
+
app_key: str | None = None,
|
|
317
|
+
record_id: str | int | None = None,
|
|
318
|
+
workflow_node_id: str | int | None = None,
|
|
319
|
+
note: str | None = None,
|
|
320
|
+
) -> dict:
|
|
321
|
+
try:
|
|
322
|
+
return trim_public_response(
|
|
323
|
+
"feedback_submit",
|
|
324
|
+
feedback.feedback_submit(
|
|
325
|
+
category=category,
|
|
326
|
+
title=title,
|
|
327
|
+
description=description,
|
|
328
|
+
expected_behavior=expected_behavior,
|
|
329
|
+
actual_behavior=actual_behavior,
|
|
330
|
+
impact_scope=impact_scope,
|
|
331
|
+
tool_name=tool_name,
|
|
332
|
+
app_key=app_key,
|
|
333
|
+
record_id=record_id,
|
|
334
|
+
workflow_node_id=workflow_node_id,
|
|
335
|
+
note=note,
|
|
336
|
+
),
|
|
337
|
+
)
|
|
338
|
+
except RuntimeError as exc:
|
|
339
|
+
try:
|
|
340
|
+
payload = json.loads(str(exc))
|
|
341
|
+
except json.JSONDecodeError:
|
|
342
|
+
raise
|
|
343
|
+
if isinstance(payload, dict):
|
|
344
|
+
raise RuntimeError(json.dumps(trim_error_response(payload), ensure_ascii=False)) from None
|
|
345
|
+
raise
|
|
346
|
+
|
|
347
|
+
code_block_tools.register(server)
|
|
348
|
+
task_context_tools.register(server)
|
|
349
|
+
directory_tools.register(server)
|
|
306
350
|
|
|
307
351
|
return server
|
|
308
352
|
|