@josephyan/qingflow-cli 0.2.0-beta.55

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.
Files changed (79) hide show
  1. package/README.md +30 -0
  2. package/docs/local-agent-install.md +235 -0
  3. package/entry_point.py +13 -0
  4. package/npm/bin/qingflow.mjs +5 -0
  5. package/npm/lib/runtime.mjs +204 -0
  6. package/npm/scripts/postinstall.mjs +16 -0
  7. package/package.json +34 -0
  8. package/pyproject.toml +67 -0
  9. package/qingflow +15 -0
  10. package/src/qingflow_mcp/__init__.py +5 -0
  11. package/src/qingflow_mcp/__main__.py +5 -0
  12. package/src/qingflow_mcp/backend_client.py +547 -0
  13. package/src/qingflow_mcp/builder_facade/__init__.py +3 -0
  14. package/src/qingflow_mcp/builder_facade/models.py +985 -0
  15. package/src/qingflow_mcp/builder_facade/service.py +8243 -0
  16. package/src/qingflow_mcp/cli/__init__.py +1 -0
  17. package/src/qingflow_mcp/cli/commands/__init__.py +15 -0
  18. package/src/qingflow_mcp/cli/commands/app.py +40 -0
  19. package/src/qingflow_mcp/cli/commands/auth.py +78 -0
  20. package/src/qingflow_mcp/cli/commands/builder.py +184 -0
  21. package/src/qingflow_mcp/cli/commands/common.py +47 -0
  22. package/src/qingflow_mcp/cli/commands/imports.py +86 -0
  23. package/src/qingflow_mcp/cli/commands/record.py +202 -0
  24. package/src/qingflow_mcp/cli/commands/task.py +87 -0
  25. package/src/qingflow_mcp/cli/commands/workspace.py +33 -0
  26. package/src/qingflow_mcp/cli/context.py +48 -0
  27. package/src/qingflow_mcp/cli/formatters.py +269 -0
  28. package/src/qingflow_mcp/cli/json_io.py +50 -0
  29. package/src/qingflow_mcp/cli/main.py +147 -0
  30. package/src/qingflow_mcp/config.py +221 -0
  31. package/src/qingflow_mcp/errors.py +66 -0
  32. package/src/qingflow_mcp/import_store.py +121 -0
  33. package/src/qingflow_mcp/json_types.py +18 -0
  34. package/src/qingflow_mcp/list_type_labels.py +76 -0
  35. package/src/qingflow_mcp/server.py +211 -0
  36. package/src/qingflow_mcp/server_app_builder.py +387 -0
  37. package/src/qingflow_mcp/server_app_user.py +317 -0
  38. package/src/qingflow_mcp/session_store.py +289 -0
  39. package/src/qingflow_mcp/solution/__init__.py +6 -0
  40. package/src/qingflow_mcp/solution/build_assembly_store.py +181 -0
  41. package/src/qingflow_mcp/solution/compiler/__init__.py +282 -0
  42. package/src/qingflow_mcp/solution/compiler/chart_compiler.py +96 -0
  43. package/src/qingflow_mcp/solution/compiler/form_compiler.py +466 -0
  44. package/src/qingflow_mcp/solution/compiler/icon_utils.py +113 -0
  45. package/src/qingflow_mcp/solution/compiler/navigation_compiler.py +57 -0
  46. package/src/qingflow_mcp/solution/compiler/package_compiler.py +19 -0
  47. package/src/qingflow_mcp/solution/compiler/portal_compiler.py +60 -0
  48. package/src/qingflow_mcp/solution/compiler/view_compiler.py +51 -0
  49. package/src/qingflow_mcp/solution/compiler/workflow_compiler.py +173 -0
  50. package/src/qingflow_mcp/solution/design_session.py +222 -0
  51. package/src/qingflow_mcp/solution/design_store.py +100 -0
  52. package/src/qingflow_mcp/solution/executor.py +2339 -0
  53. package/src/qingflow_mcp/solution/normalizer.py +23 -0
  54. package/src/qingflow_mcp/solution/requirements_builder.py +536 -0
  55. package/src/qingflow_mcp/solution/run_store.py +244 -0
  56. package/src/qingflow_mcp/solution/spec_models.py +853 -0
  57. package/src/qingflow_mcp/tools/__init__.py +1 -0
  58. package/src/qingflow_mcp/tools/ai_builder_tools.py +2063 -0
  59. package/src/qingflow_mcp/tools/app_tools.py +850 -0
  60. package/src/qingflow_mcp/tools/approval_tools.py +833 -0
  61. package/src/qingflow_mcp/tools/auth_tools.py +697 -0
  62. package/src/qingflow_mcp/tools/base.py +81 -0
  63. package/src/qingflow_mcp/tools/code_block_tools.py +679 -0
  64. package/src/qingflow_mcp/tools/directory_tools.py +648 -0
  65. package/src/qingflow_mcp/tools/feedback_tools.py +230 -0
  66. package/src/qingflow_mcp/tools/file_tools.py +385 -0
  67. package/src/qingflow_mcp/tools/import_tools.py +1971 -0
  68. package/src/qingflow_mcp/tools/navigation_tools.py +177 -0
  69. package/src/qingflow_mcp/tools/package_tools.py +240 -0
  70. package/src/qingflow_mcp/tools/portal_tools.py +131 -0
  71. package/src/qingflow_mcp/tools/qingbi_report_tools.py +269 -0
  72. package/src/qingflow_mcp/tools/record_tools.py +12739 -0
  73. package/src/qingflow_mcp/tools/role_tools.py +94 -0
  74. package/src/qingflow_mcp/tools/solution_tools.py +3887 -0
  75. package/src/qingflow_mcp/tools/task_context_tools.py +1423 -0
  76. package/src/qingflow_mcp/tools/task_tools.py +843 -0
  77. package/src/qingflow_mcp/tools/view_tools.py +280 -0
  78. package/src/qingflow_mcp/tools/workflow_tools.py +312 -0
  79. package/src/qingflow_mcp/tools/workspace_tools.py +219 -0
@@ -0,0 +1,177 @@
1
+ from __future__ import annotations
2
+
3
+ from mcp.server.fastmcp import FastMCP
4
+
5
+ from ..config import DEFAULT_PROFILE
6
+ from ..errors import QingflowApiError, raise_tool_error
7
+ from ..json_types import JSONObject
8
+ from .base import ToolBase
9
+
10
+
11
+ class NavigationTools(ToolBase):
12
+ def register(self, mcp: FastMCP) -> None:
13
+ @mcp.tool()
14
+ def navigation_list_published(profile: str = DEFAULT_PROFILE, page_num: int = 1, page_size: int = 50) -> JSONObject:
15
+ return self.navigation_list_published(profile=profile, page_num=page_num, page_size=page_size)
16
+
17
+ @mcp.tool()
18
+ def navigation_list_draft_page(
19
+ profile: str = DEFAULT_PROFILE,
20
+ page_num: int = 1,
21
+ page_size: int = 50,
22
+ query_key: str | None = None,
23
+ ) -> JSONObject:
24
+ return self.navigation_list_draft_page(profile=profile, page_num=page_num, page_size=page_size, query_key=query_key)
25
+
26
+ @mcp.tool()
27
+ def navigation_list_draft_all(profile: str = DEFAULT_PROFILE, query_key: str | None = None) -> JSONObject:
28
+ return self.navigation_list_draft_all(profile=profile, query_key=query_key)
29
+
30
+ @mcp.tool()
31
+ def navigation_get_detail(profile: str = DEFAULT_PROFILE, navigation_item_id: int = 0, status: str = "draft") -> JSONObject:
32
+ return self.navigation_get_detail(profile=profile, navigation_item_id=navigation_item_id, status=status)
33
+
34
+ @mcp.tool()
35
+ def navigation_get_status(profile: str = DEFAULT_PROFILE) -> JSONObject:
36
+ return self.navigation_get_status(profile=profile)
37
+
38
+ @mcp.tool()
39
+ def navigation_set_visible(profile: str = DEFAULT_PROFILE, payload: JSONObject | None = None) -> JSONObject:
40
+ return self.navigation_set_visible(profile=profile, payload=payload or {})
41
+
42
+ @mcp.tool()
43
+ def navigation_create(profile: str = DEFAULT_PROFILE, payload: JSONObject | None = None) -> JSONObject:
44
+ return self.navigation_create(profile=profile, payload=payload or {})
45
+
46
+ @mcp.tool(description=self._high_risk_tool_description(operation="update", target="navigation item configuration"))
47
+ def navigation_update(profile: str = DEFAULT_PROFILE, navigation_item_id: int = 0, payload: JSONObject | None = None) -> JSONObject:
48
+ return self.navigation_update(profile=profile, navigation_item_id=navigation_item_id, payload=payload or {})
49
+
50
+ @mcp.tool(description=self._high_risk_tool_description(operation="delete", target="navigation item configuration"))
51
+ def navigation_delete(profile: str = DEFAULT_PROFILE, navigation_item_id: int = 0) -> JSONObject:
52
+ return self.navigation_delete(profile=profile, navigation_item_id=navigation_item_id)
53
+
54
+ @mcp.tool()
55
+ def navigation_publish(profile: str = DEFAULT_PROFILE, navigation_id: int = 0) -> JSONObject:
56
+ return self.navigation_publish(profile=profile, navigation_id=navigation_id)
57
+
58
+ @mcp.tool()
59
+ def navigation_reorder(profile: str = DEFAULT_PROFILE, payload: list[JSONObject] | None = None) -> JSONObject:
60
+ return self.navigation_reorder(profile=profile, payload=payload or [])
61
+
62
+ def navigation_list_published(self, *, profile: str, page_num: int = 1, page_size: int = 50) -> JSONObject:
63
+ def runner(session_profile, context):
64
+ result = self.backend.request("GET", context, "/navigation", params={"pageNum": page_num, "pageSize": page_size})
65
+ return {"profile": profile, "ws_id": session_profile.selected_ws_id, "page": result}
66
+
67
+ return self._run(profile, runner)
68
+
69
+ def navigation_list_draft_page(self, *, profile: str, page_num: int = 1, page_size: int = 50, query_key: str | None = None) -> JSONObject:
70
+ def runner(session_profile, context):
71
+ params: JSONObject = {"pageNum": page_num, "pageSize": page_size}
72
+ if query_key:
73
+ params["queryCondition"] = query_key
74
+ result = self.backend.request("GET", context, "/navigation/page", params=params)
75
+ return {"profile": profile, "ws_id": session_profile.selected_ws_id, "page": result}
76
+
77
+ return self._run(profile, runner)
78
+
79
+ def navigation_list_draft_all(self, *, profile: str, query_key: str | None = None) -> JSONObject:
80
+ def runner(session_profile, context):
81
+ params: JSONObject = {}
82
+ if query_key:
83
+ params["queryCondition"] = query_key
84
+ result = self.backend.request("GET", context, "/navigation/all", params=params)
85
+ return {"profile": profile, "ws_id": session_profile.selected_ws_id, "items": result}
86
+
87
+ return self._run(profile, runner)
88
+
89
+ def navigation_get_detail(self, *, profile: str, navigation_item_id: int, status: str = "draft") -> JSONObject:
90
+ self._require_navigation_item_id(navigation_item_id)
91
+
92
+ def runner(session_profile, context):
93
+ result = self.backend.request(
94
+ "GET",
95
+ context,
96
+ f"/navigation/detail/{navigation_item_id}",
97
+ params={"status": status},
98
+ )
99
+ return {"profile": profile, "ws_id": session_profile.selected_ws_id, "navigation_item_id": navigation_item_id, "result": result}
100
+
101
+ return self._run(profile, runner)
102
+
103
+ def navigation_get_status(self, *, profile: str) -> JSONObject:
104
+ def runner(session_profile, context):
105
+ result = self.backend.request("GET", context, "/navigation/status")
106
+ return {"profile": profile, "ws_id": session_profile.selected_ws_id, "result": result}
107
+
108
+ return self._run(profile, runner)
109
+
110
+ def navigation_set_visible(self, *, profile: str, payload: JSONObject) -> JSONObject:
111
+ body = self._require_dict(payload)
112
+
113
+ def runner(session_profile, context):
114
+ result = self.backend.request("POST", context, "/navigation/visible", json_body=body)
115
+ return {"profile": profile, "ws_id": session_profile.selected_ws_id, "result": result}
116
+
117
+ return self._run(profile, runner)
118
+
119
+ def navigation_create(self, *, profile: str, payload: JSONObject) -> JSONObject:
120
+ body = self._require_dict(payload)
121
+
122
+ def runner(session_profile, context):
123
+ result = self.backend.request("POST", context, "/navigation", json_body=body)
124
+ return {"profile": profile, "ws_id": session_profile.selected_ws_id, "result": result}
125
+
126
+ return self._run(profile, runner)
127
+
128
+ def navigation_update(self, *, profile: str, navigation_item_id: int, payload: JSONObject) -> JSONObject:
129
+ self._require_navigation_item_id(navigation_item_id)
130
+ body = self._require_dict(payload)
131
+
132
+ def runner(session_profile, context):
133
+ result = self.backend.request("PUT", context, f"/navigation/{navigation_item_id}", json_body=body)
134
+ return self._attach_human_review_notice(
135
+ {"profile": profile, "ws_id": session_profile.selected_ws_id, "navigation_item_id": navigation_item_id, "result": result},
136
+ operation="update",
137
+ target="navigation item configuration",
138
+ )
139
+
140
+ return self._run(profile, runner)
141
+
142
+ def navigation_delete(self, *, profile: str, navigation_item_id: int) -> JSONObject:
143
+ self._require_navigation_item_id(navigation_item_id)
144
+
145
+ def runner(session_profile, context):
146
+ result = self.backend.request("DELETE", context, f"/navigation/{navigation_item_id}")
147
+ return self._attach_human_review_notice(
148
+ {"profile": profile, "ws_id": session_profile.selected_ws_id, "navigation_item_id": navigation_item_id, "result": result},
149
+ operation="delete",
150
+ target="navigation item configuration",
151
+ )
152
+
153
+ return self._run(profile, runner)
154
+
155
+ def navigation_publish(self, *, profile: str, navigation_id: int) -> JSONObject:
156
+ if navigation_id <= 0:
157
+ raise_tool_error(QingflowApiError.config_error("navigation_id must be positive"))
158
+
159
+ def runner(session_profile, context):
160
+ result = self.backend.request("POST", context, f"/navigation/publish/{navigation_id}")
161
+ return {"profile": profile, "ws_id": session_profile.selected_ws_id, "navigation_id": navigation_id, "result": result}
162
+
163
+ return self._run(profile, runner)
164
+
165
+ def navigation_reorder(self, *, profile: str, payload: list[JSONObject]) -> JSONObject:
166
+ if not isinstance(payload, list) or not payload:
167
+ raise_tool_error(QingflowApiError.config_error("payload must be a non-empty array"))
168
+
169
+ def runner(session_profile, context):
170
+ result = self.backend.request("POST", context, "/navigation/ordinal", json_body=payload)
171
+ return {"profile": profile, "ws_id": session_profile.selected_ws_id, "result": result}
172
+
173
+ return self._run(profile, runner)
174
+
175
+ def _require_navigation_item_id(self, navigation_item_id: int) -> None:
176
+ if navigation_item_id <= 0:
177
+ raise_tool_error(QingflowApiError.config_error("navigation_item_id must be positive"))
@@ -0,0 +1,240 @@
1
+ from __future__ import annotations
2
+
3
+ from copy import deepcopy
4
+
5
+ from mcp.server.fastmcp import FastMCP
6
+
7
+ from ..config import DEFAULT_PROFILE
8
+ from ..errors import QingflowApiError, raise_tool_error
9
+ from ..json_types import JSONObject
10
+ from .base import ToolBase
11
+
12
+
13
+ class PackageTools(ToolBase):
14
+ def register(self, mcp: FastMCP) -> None:
15
+ @mcp.tool()
16
+ def package_list(profile: str = DEFAULT_PROFILE, trial_status: str = "all", include_raw: bool = False) -> JSONObject:
17
+ return self.package_list(profile=profile, trial_status=trial_status, include_raw=include_raw)
18
+
19
+ @mcp.tool()
20
+ def package_get(profile: str = DEFAULT_PROFILE, tag_id: int = 0, include_raw: bool = False) -> JSONObject:
21
+ return self.package_get(profile=profile, tag_id=tag_id, include_raw=include_raw)
22
+
23
+ @mcp.tool()
24
+ def package_get_base(profile: str = DEFAULT_PROFILE, tag_id: int = 0, include_raw: bool = False) -> JSONObject:
25
+ return self.package_get_base(profile=profile, tag_id=tag_id, include_raw=include_raw)
26
+
27
+ @mcp.tool()
28
+ def package_create(profile: str = DEFAULT_PROFILE, payload: JSONObject | None = None) -> JSONObject:
29
+ return self.package_create(profile=profile, payload=payload or {})
30
+
31
+ @mcp.tool(description=self._high_risk_tool_description(operation="update", target="app package settings"))
32
+ def package_update(profile: str = DEFAULT_PROFILE, tag_id: int = 0, payload: JSONObject | None = None) -> JSONObject:
33
+ return self.package_update(profile=profile, tag_id=tag_id, payload=payload or {})
34
+
35
+ @mcp.tool(description=self._high_risk_tool_description(operation="delete", target="app package and linked data"))
36
+ def package_delete(profile: str = DEFAULT_PROFILE, tag_id: int = 0, deleted_all_data: bool = False) -> JSONObject:
37
+ return self.package_delete(profile=profile, tag_id=tag_id, deleted_all_data=deleted_all_data)
38
+
39
+ def package_list(self, *, profile: str, trial_status: str = "all", include_raw: bool = False) -> JSONObject:
40
+ def runner(session_profile, context):
41
+ result = self.backend.request("GET", context, "/tag", params={"trialStatus": trial_status})
42
+ raw_items, source_shape = self._extract_package_items(result)
43
+ compact_items = [self._compact_package(item) for item in raw_items if isinstance(item, dict)]
44
+ response = {
45
+ "profile": profile,
46
+ "ws_id": session_profile.selected_ws_id,
47
+ "trial_status": trial_status,
48
+ "items": result if include_raw else compact_items,
49
+ "count": len(compact_items),
50
+ "compact": not include_raw,
51
+ "source_shape": source_shape,
52
+ "retried": False,
53
+ }
54
+ if include_raw:
55
+ response["summary"] = compact_items
56
+ return response
57
+
58
+ return self._run(profile, runner)
59
+
60
+ def package_get(self, *, profile: str, tag_id: int, include_raw: bool = False) -> JSONObject:
61
+ self._require_tag_id(tag_id)
62
+
63
+ def runner(session_profile, context):
64
+ result = self.backend.request("GET", context, f"/tag/{tag_id}")
65
+ base_info = self.backend.request("GET", context, f"/tag/{tag_id}/baseInfo")
66
+ compact = self._compact_package(
67
+ result if isinstance(result, dict) else {},
68
+ base_info=base_info if isinstance(base_info, dict) else None,
69
+ )
70
+ response = {
71
+ "profile": profile,
72
+ "ws_id": session_profile.selected_ws_id,
73
+ "tag_id": tag_id,
74
+ "result": result if include_raw else compact,
75
+ "compact": not include_raw,
76
+ }
77
+ if include_raw:
78
+ response["summary"] = compact
79
+ return response
80
+
81
+ return self._run(profile, runner)
82
+
83
+ def package_get_base(self, *, profile: str, tag_id: int, include_raw: bool = False) -> JSONObject:
84
+ self._require_readable_tag_id(tag_id)
85
+
86
+ def runner(session_profile, context):
87
+ result = self.backend.request("GET", context, f"/tag/{tag_id}/baseInfo")
88
+ compact = self._compact_package(result if isinstance(result, dict) else {})
89
+ response = {
90
+ "profile": profile,
91
+ "ws_id": session_profile.selected_ws_id,
92
+ "tag_id": tag_id,
93
+ "result": result if include_raw else compact,
94
+ "compact": not include_raw,
95
+ }
96
+ if include_raw:
97
+ response["summary"] = compact
98
+ return response
99
+
100
+ return self._run(profile, runner)
101
+
102
+ def package_create(self, *, profile: str, payload: JSONObject) -> JSONObject:
103
+ body = self._require_dict(payload)
104
+
105
+ def runner(session_profile, context):
106
+ result = self.backend.request("POST", context, "/tag", json_body=self._normalize_package_payload(body))
107
+ return {"profile": profile, "ws_id": session_profile.selected_ws_id, "result": result}
108
+
109
+ return self._run(profile, runner)
110
+
111
+ def package_update(self, *, profile: str, tag_id: int, payload: JSONObject) -> JSONObject:
112
+ self._require_tag_id(tag_id)
113
+ body = self._require_dict(payload)
114
+
115
+ def runner(session_profile, context):
116
+ current = self.backend.request("GET", context, f"/tag/{tag_id}")
117
+ result = self.backend.request(
118
+ "PUT",
119
+ context,
120
+ f"/tag/{tag_id}",
121
+ json_body=self._normalize_package_payload(body, existing=current),
122
+ )
123
+ return self._attach_human_review_notice(
124
+ {"profile": profile, "ws_id": session_profile.selected_ws_id, "tag_id": tag_id, "result": result},
125
+ operation="update",
126
+ target="app package settings",
127
+ )
128
+
129
+ return self._run(profile, runner)
130
+
131
+ def package_sort_items(self, *, profile: str, tag_id: int, tag_items: list[JSONObject]) -> JSONObject:
132
+ self._require_tag_id(tag_id)
133
+
134
+ def runner(session_profile, context):
135
+ result = self.backend.request(
136
+ "POST",
137
+ context,
138
+ f"/tag/{tag_id}/ordinal",
139
+ json_body={"tagItems": deepcopy(tag_items)},
140
+ )
141
+ return {"profile": profile, "ws_id": session_profile.selected_ws_id, "tag_id": tag_id, "result": result}
142
+
143
+ return self._run(profile, runner)
144
+
145
+ def package_delete(self, *, profile: str, tag_id: int, deleted_all_data: bool = False) -> JSONObject:
146
+ self._require_tag_id(tag_id)
147
+
148
+ def runner(session_profile, context):
149
+ result = self.backend.request(
150
+ "DELETE",
151
+ context,
152
+ "/tag",
153
+ json_body={"tagId": tag_id, "deletedAllData": deleted_all_data},
154
+ )
155
+ return self._attach_human_review_notice(
156
+ {"profile": profile, "ws_id": session_profile.selected_ws_id, "tag_id": tag_id, "deleted_all_data": deleted_all_data, "result": result},
157
+ operation="delete",
158
+ target="app package and linked data",
159
+ )
160
+
161
+ return self._run(profile, runner)
162
+
163
+ def _require_tag_id(self, tag_id: int) -> None:
164
+ if tag_id <= 0:
165
+ raise_tool_error(QingflowApiError.config_error("tag_id must be positive"))
166
+
167
+ def _require_readable_tag_id(self, tag_id: int) -> None:
168
+ if tag_id < 0:
169
+ raise_tool_error(QingflowApiError.config_error("tag_id must be non-negative"))
170
+
171
+ def _normalize_package_payload(self, payload: JSONObject, existing: JSONObject | None = None) -> JSONObject:
172
+ data = deepcopy(existing) if isinstance(existing, dict) else {}
173
+ data.update(deepcopy(payload))
174
+ data.pop("tagId", None)
175
+ data.pop("createTime", None)
176
+ data.pop("creator", None)
177
+ data.pop("publishStatus", None)
178
+ data.pop("beingTrial", None)
179
+ data.pop("trialExpireDate", None)
180
+ data.setdefault("tagIcon", "")
181
+ data.setdefault("tagItems", [])
182
+ data.setdefault("auth", _default_package_auth())
183
+ return data
184
+
185
+ def _compact_package(self, result: dict[str, object], *, base_info: dict[str, object] | None = None) -> JSONObject:
186
+ tag_items = result.get("tagItems") if isinstance(result.get("tagItems"), list) else []
187
+ permission_source = base_info if isinstance(base_info, dict) else result
188
+ preview = []
189
+ for item in tag_items[:10]:
190
+ if not isinstance(item, dict):
191
+ continue
192
+ preview.append(
193
+ {
194
+ "title": item.get("title"),
195
+ "itemType": item.get("itemType"),
196
+ "appKey": item.get("appKey"),
197
+ "dashKey": item.get("dashKey"),
198
+ }
199
+ )
200
+ compact = {
201
+ "tagId": result.get("tagId"),
202
+ "tagName": result.get("tagName"),
203
+ "publishStatus": result.get("publishStatus") if result.get("publishStatus") is not None else permission_source.get("publishStatus"),
204
+ "beingTrial": result.get("beingTrial"),
205
+ "itemCount": len(tag_items),
206
+ "itemPreview": preview,
207
+ "addAppStatus": permission_source.get("addAppStatus"),
208
+ "editAppStatus": permission_source.get("editAppStatus"),
209
+ "delAppStatus": permission_source.get("delAppStatus"),
210
+ "editTagStatus": permission_source.get("editTagStatus"),
211
+ }
212
+ return {key: value for key, value in compact.items() if value is not None}
213
+
214
+ def _extract_package_items(self, result: object) -> tuple[list[dict[str, object]], str]:
215
+ if isinstance(result, list):
216
+ return [item for item in result if isinstance(item, dict)], "list"
217
+ if isinstance(result, dict):
218
+ tags = result.get("tags")
219
+ if isinstance(tags, list):
220
+ return [item for item in tags if isinstance(item, dict)], "dict.tags"
221
+ items = result.get("items")
222
+ if isinstance(items, list):
223
+ return [item for item in items if isinstance(item, dict)], "dict.items"
224
+ return [], "dict.unknown"
225
+ return [], type(result).__name__
226
+
227
+
228
+ def _default_package_auth() -> JSONObject:
229
+ members = {
230
+ "depart": [],
231
+ "dynamic": [],
232
+ "includeSubDeparts": None,
233
+ "member": [],
234
+ "role": [],
235
+ }
236
+ return {
237
+ "type": "WORKSPACE",
238
+ "contactAuth": {"type": "WORKSPACE_ALL", "authMembers": deepcopy(members)},
239
+ "externalMemberAuth": {"type": "NOT", "authMembers": deepcopy(members)},
240
+ }
@@ -0,0 +1,131 @@
1
+ from __future__ import annotations
2
+
3
+ from mcp.server.fastmcp import FastMCP
4
+
5
+ from ..config import DEFAULT_PROFILE
6
+ from ..errors import QingflowApiError, raise_tool_error
7
+ from ..json_types import JSONObject
8
+ from .base import ToolBase
9
+
10
+
11
+ class PortalTools(ToolBase):
12
+ def register(self, mcp: FastMCP) -> None:
13
+ @mcp.tool()
14
+ def portal_list(profile: str = DEFAULT_PROFILE) -> JSONObject:
15
+ return self.portal_list(profile=profile)
16
+
17
+ @mcp.tool()
18
+ def portal_get(profile: str = DEFAULT_PROFILE, dash_key: str = "", being_draft: bool = False) -> JSONObject:
19
+ return self.portal_get(profile=profile, dash_key=dash_key, being_draft=being_draft)
20
+
21
+ @mcp.tool()
22
+ def portal_get_base_info(profile: str = DEFAULT_PROFILE, dash_key: str = "") -> JSONObject:
23
+ return self.portal_get_base_info(profile=profile, dash_key=dash_key)
24
+
25
+ @mcp.tool()
26
+ def portal_create(profile: str = DEFAULT_PROFILE, payload: JSONObject | None = None) -> JSONObject:
27
+ return self.portal_create(profile=profile, payload=payload or {})
28
+
29
+ @mcp.tool(description=self._high_risk_tool_description(operation="update", target="portal base settings"))
30
+ def portal_update_base_info(profile: str = DEFAULT_PROFILE, dash_key: str = "", payload: JSONObject | None = None) -> JSONObject:
31
+ return self.portal_update_base_info(profile=profile, dash_key=dash_key, payload=payload or {})
32
+
33
+ @mcp.tool(description=self._high_risk_tool_description(operation="update", target="portal configuration"))
34
+ def portal_update(profile: str = DEFAULT_PROFILE, dash_key: str = "", payload: JSONObject | None = None) -> JSONObject:
35
+ return self.portal_update(profile=profile, dash_key=dash_key, payload=payload or {})
36
+
37
+ @mcp.tool(description=self._high_risk_tool_description(operation="delete", target="portal configuration"))
38
+ def portal_delete(profile: str = DEFAULT_PROFILE, dash_key: str = "") -> JSONObject:
39
+ return self.portal_delete(profile=profile, dash_key=dash_key)
40
+
41
+ @mcp.tool()
42
+ def portal_publish(profile: str = DEFAULT_PROFILE, dash_key: str = "") -> JSONObject:
43
+ return self.portal_publish(profile=profile, dash_key=dash_key)
44
+
45
+ def portal_list(self, *, profile: str) -> JSONObject:
46
+ def runner(session_profile, context):
47
+ result = self.backend.request("GET", context, "/dash")
48
+ return {"profile": profile, "ws_id": session_profile.selected_ws_id, "items": result}
49
+
50
+ return self._run(profile, runner)
51
+
52
+ def portal_get(self, *, profile: str, dash_key: str, being_draft: bool = False) -> JSONObject:
53
+ self._require_dash_key(dash_key)
54
+
55
+ def runner(session_profile, context):
56
+ result = self.backend.request("GET", context, f"/dash/{dash_key}", params={"beingDraft": being_draft})
57
+ return {"profile": profile, "ws_id": session_profile.selected_ws_id, "dash_key": dash_key, "result": result}
58
+
59
+ return self._run(profile, runner)
60
+
61
+ def portal_create(self, *, profile: str, payload: JSONObject) -> JSONObject:
62
+ body = self._require_dict(payload)
63
+
64
+ def runner(session_profile, context):
65
+ result = self.backend.request("POST", context, "/dash", json_body=body)
66
+ return {"profile": profile, "ws_id": session_profile.selected_ws_id, "result": result}
67
+
68
+ return self._run(profile, runner)
69
+
70
+ def portal_get_base_info(self, *, profile: str, dash_key: str) -> JSONObject:
71
+ self._require_dash_key(dash_key)
72
+
73
+ def runner(session_profile, context):
74
+ result = self.backend.request("GET", context, f"/dash/{dash_key}/baseInfo")
75
+ return {"profile": profile, "ws_id": session_profile.selected_ws_id, "dash_key": dash_key, "result": result}
76
+
77
+ return self._run(profile, runner)
78
+
79
+ def portal_update_base_info(self, *, profile: str, dash_key: str, payload: JSONObject) -> JSONObject:
80
+ self._require_dash_key(dash_key)
81
+ body = self._require_dict(payload)
82
+
83
+ def runner(session_profile, context):
84
+ result = self.backend.request("POST", context, f"/dash/{dash_key}/baseInfo", json_body=body)
85
+ return self._attach_human_review_notice(
86
+ {"profile": profile, "ws_id": session_profile.selected_ws_id, "dash_key": dash_key, "result": result},
87
+ operation="update",
88
+ target="portal base settings",
89
+ )
90
+
91
+ return self._run(profile, runner)
92
+
93
+ def portal_update(self, *, profile: str, dash_key: str, payload: JSONObject) -> JSONObject:
94
+ self._require_dash_key(dash_key)
95
+ body = self._require_dict(payload)
96
+
97
+ def runner(session_profile, context):
98
+ result = self.backend.request("POST", context, f"/dash/{dash_key}", json_body=body)
99
+ return self._attach_human_review_notice(
100
+ {"profile": profile, "ws_id": session_profile.selected_ws_id, "dash_key": dash_key, "result": result},
101
+ operation="update",
102
+ target="portal configuration",
103
+ )
104
+
105
+ return self._run(profile, runner)
106
+
107
+ def portal_delete(self, *, profile: str, dash_key: str) -> JSONObject:
108
+ self._require_dash_key(dash_key)
109
+
110
+ def runner(session_profile, context):
111
+ result = self.backend.request("DELETE", context, f"/dash/{dash_key}")
112
+ return self._attach_human_review_notice(
113
+ {"profile": profile, "ws_id": session_profile.selected_ws_id, "dash_key": dash_key, "result": result},
114
+ operation="delete",
115
+ target="portal configuration",
116
+ )
117
+
118
+ return self._run(profile, runner)
119
+
120
+ def portal_publish(self, *, profile: str, dash_key: str) -> JSONObject:
121
+ self._require_dash_key(dash_key)
122
+
123
+ def runner(session_profile, context):
124
+ result = self.backend.request("POST", context, f"/dash/{dash_key}/publish")
125
+ return {"profile": profile, "ws_id": session_profile.selected_ws_id, "dash_key": dash_key, "result": result}
126
+
127
+ return self._run(profile, runner)
128
+
129
+ def _require_dash_key(self, dash_key: str) -> None:
130
+ if not dash_key:
131
+ raise_tool_error(QingflowApiError.config_error("dash_key is required"))