@josephyan/qingflow-app-user-mcp 0.2.0-beta.31 → 0.2.0-beta.33
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/skills/qingflow-app-user/SKILL.md +7 -2
- package/skills/qingflow-task-ops/SKILL.md +51 -62
- package/src/qingflow_mcp/__init__.py +1 -1
- package/src/qingflow_mcp/config.py +38 -0
- package/src/qingflow_mcp/server.py +21 -1
- package/src/qingflow_mcp/server_app_builder.py +6 -1
- package/src/qingflow_mcp/server_app_user.py +23 -2
- package/src/qingflow_mcp/tools/approval_tools.py +54 -1
- package/src/qingflow_mcp/tools/feedback_tools.py +224 -0
- package/src/qingflow_mcp/tools/record_tools.py +4 -1
- package/src/qingflow_mcp/tools/task_context_tools.py +1063 -0
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.
|
|
6
|
+
npm install @josephyan/qingflow-app-user-mcp@0.2.0-beta.33
|
|
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.
|
|
12
|
+
npx -y -p @josephyan/qingflow-app-user-mcp@0.2.0-beta.33 qingflow-app-user-mcp
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
Environment:
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
|
@@ -19,16 +19,20 @@ Route to exactly one of these specialized paths:
|
|
|
19
19
|
1. Record CRUD
|
|
20
20
|
Switch to [$qingflow-record-crud](/Users/yanqidong/Documents/qingflow-next/.codex/skills/qingflow-record-crud/SKILL.md)
|
|
21
21
|
|
|
22
|
-
2.
|
|
22
|
+
2. Task workflow operations
|
|
23
|
+
Switch to [$qingflow-task-ops](/Users/yanqidong/Documents/qingflow-next/.codex/skills/qingflow-task-ops/SKILL.md)
|
|
24
|
+
|
|
25
|
+
3. Analysis
|
|
23
26
|
Switch to [$qingflow-record-analysis](/Users/yanqidong/Documents/qingflow-next/.codex/skills/qingflow-record-analysis/SKILL.md)
|
|
24
27
|
|
|
25
|
-
|
|
28
|
+
4. MCP connection / auth / workspace selection
|
|
26
29
|
Switch to [$qingflow-mcp-setup](/Users/yanqidong/.codex/skills/qingflow-mcp-setup/SKILL.md)
|
|
27
30
|
|
|
28
31
|
## Routing Rules
|
|
29
32
|
|
|
30
33
|
- If the user does not know the target `app_key`, discover apps first with `app_list` or `app_search`, then route to the specialized skill
|
|
31
34
|
- If the task is about browsing, reading, creating, updating, deleting, attachments, relations, subtable writes, or member/department-field candidate lookup, switch to `$qingflow-record-crud`
|
|
35
|
+
- If the task is about todo discovery, task context, approval actions, rollback or transfer, associated report review, or workflow log review, switch to `$qingflow-task-ops`
|
|
32
36
|
- If the task is about grouped distributions, ratios, rankings, trends, insights, or any final statistical conclusion, switch to `$qingflow-record-analysis`
|
|
33
37
|
- If the MCP is not connected, authenticated, or bound to the right workspace, switch to `$qingflow-mcp-setup`
|
|
34
38
|
|
|
@@ -37,6 +41,7 @@ Route to exactly one of these specialized paths:
|
|
|
37
41
|
- prefer canonical app ids, record ids, task ids, and workflow node ids over guessed names
|
|
38
42
|
- if a field or target is still ambiguous after schema/task lookup, ask the user to confirm from a short candidate list instead of guessing
|
|
39
43
|
- if the task can stay read-only, do not write or act
|
|
44
|
+
- if the current MCP capability is unsupported, the workflow is awkward, or the user's need still cannot be satisfied after reasonable use, summarize the gap, ask whether to submit feedback, and call `feedback_submit` only after explicit user confirmation
|
|
40
45
|
|
|
41
46
|
## Resources
|
|
42
47
|
|
|
@@ -1,82 +1,62 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: qingflow-task-ops
|
|
3
|
-
description: Use Qingflow
|
|
3
|
+
description: Use Qingflow todo discovery, workflow task context, associated approval context, workflow logs, and unified task actions after the MCP is already connected and authenticated. Do not use this skill for record CRUD or final statistical analysis.
|
|
4
4
|
metadata:
|
|
5
|
-
short-description: Qingflow task
|
|
5
|
+
short-description: Qingflow task workflow context and actions
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Qingflow Task Ops
|
|
9
9
|
|
|
10
10
|
## Overview
|
|
11
11
|
|
|
12
|
-
This skill is for task
|
|
12
|
+
This skill is for task workflow operations only.
|
|
13
13
|
Assumes MCP is connected, authenticated, and on the correct workspace.
|
|
14
14
|
|
|
15
15
|
## Default Paths
|
|
16
16
|
|
|
17
17
|
Use exactly one of these default paths:
|
|
18
18
|
|
|
19
|
-
1.
|
|
20
|
-
`task_summary`
|
|
21
|
-
|
|
22
|
-
2. Flat task browsing
|
|
19
|
+
1. Find target todos
|
|
23
20
|
`task_list`
|
|
24
21
|
|
|
25
|
-
|
|
26
|
-
`
|
|
22
|
+
2. Read one task context
|
|
23
|
+
`task_list -> exact target -> task_get`
|
|
27
24
|
|
|
28
|
-
|
|
29
|
-
`
|
|
25
|
+
3. Read associated approval context
|
|
26
|
+
`task_get -> task_associated_report_detail_get` or `task_workflow_log_get`
|
|
30
27
|
|
|
31
|
-
|
|
32
|
-
`
|
|
28
|
+
4. Execute workflow action
|
|
29
|
+
`task_list -> exact target -> task_get -> task_action_execute`
|
|
33
30
|
|
|
34
31
|
## Core Tools
|
|
35
32
|
|
|
36
|
-
- `task_summary`
|
|
37
33
|
- `task_list`
|
|
38
|
-
- `
|
|
39
|
-
- `
|
|
40
|
-
- `
|
|
41
|
-
- `
|
|
42
|
-
- `task_approve`
|
|
43
|
-
- `task_reject`
|
|
44
|
-
- `task_rollback_candidates`
|
|
45
|
-
- `task_rollback`
|
|
46
|
-
- `task_transfer_candidates`
|
|
47
|
-
- `task_transfer`
|
|
48
|
-
- `record_comment_write`
|
|
49
|
-
- `record_comment_list`
|
|
50
|
-
- `record_comment_mentions`
|
|
51
|
-
- `record_comment_mark_read`
|
|
34
|
+
- `task_get`
|
|
35
|
+
- `task_action_execute`
|
|
36
|
+
- `task_associated_report_detail_get`
|
|
37
|
+
- `task_workflow_log_get`
|
|
52
38
|
|
|
53
39
|
## Supporting Tools
|
|
54
40
|
|
|
55
|
-
- `
|
|
56
|
-
- `
|
|
57
|
-
- `directory_list_all_internal_users`
|
|
58
|
-
- `directory_list_internal_departments`
|
|
59
|
-
- `directory_list_all_departments`
|
|
60
|
-
- `directory_list_sub_departments`
|
|
61
|
-
- `directory_list_external_members`
|
|
62
|
-
- `record_get`
|
|
41
|
+
- `app_list`
|
|
42
|
+
- `app_search`
|
|
63
43
|
|
|
64
44
|
## Standard Operating Order
|
|
65
45
|
|
|
66
46
|
1. Ensure auth exists
|
|
67
47
|
2. Ensure workspace is selected
|
|
68
|
-
3.
|
|
69
|
-
4.
|
|
70
|
-
5.
|
|
71
|
-
6.
|
|
72
|
-
7.
|
|
73
|
-
8.
|
|
48
|
+
3. Discover the exact target with `task_list`
|
|
49
|
+
4. Read node context with `task_get`
|
|
50
|
+
5. Before giving any approval recommendation, read `task_workflow_log_get`
|
|
51
|
+
6. If `task_get` returns any `associated_reports`, read every visible report through `task_associated_report_detail_get`
|
|
52
|
+
7. Give an approval recommendation only after reviewing the node context, workflow log, and associated report details
|
|
53
|
+
8. Wait for explicit user confirmation before `task_action_execute`
|
|
54
|
+
9. Execute through `task_action_execute`
|
|
55
|
+
10. After actions, report the exact `app_key`, `record_id`, `workflow_node_id`, and executed action
|
|
74
56
|
|
|
75
57
|
## Task-Center Rules
|
|
76
58
|
|
|
77
|
-
- Use `task_summary` for headline counts
|
|
78
59
|
- Use `task_list` for flat browsing
|
|
79
|
-
- Use `task_facets` for grouped worksheet or workflow-node buckets
|
|
80
60
|
- `task_box` must be one of:
|
|
81
61
|
- `todo`
|
|
82
62
|
- `initiated`
|
|
@@ -93,30 +73,39 @@ Use exactly one of these default paths:
|
|
|
93
73
|
- `due_soon`
|
|
94
74
|
- `unread`
|
|
95
75
|
- `ended`
|
|
96
|
-
-
|
|
97
|
-
-
|
|
98
|
-
-
|
|
76
|
+
- `task_list` is the only public task discovery path in this MCP surface
|
|
77
|
+
- Treat `task_id` as a locator only; the action primary key is `app_key + record_id + workflow_node_id`
|
|
78
|
+
- Default box usage:
|
|
79
|
+
- `todo`: `task_list -> task_get -> task_workflow_log_get / task_associated_report_detail_get -> recommendation -> explicit user confirmation -> task_action_execute`
|
|
80
|
+
- `initiated`: `task_list -> record_get`
|
|
81
|
+
- `done`: `task_list -> record_get`
|
|
82
|
+
- `cc`: `task_list -> record_get`
|
|
83
|
+
- Treat `initiated`, `done`, and `cc` primarily as list-plus-record-detail flows, not task action flows
|
|
99
84
|
|
|
100
85
|
## Workflow Usage Actions
|
|
101
86
|
|
|
102
|
-
-
|
|
103
|
-
-
|
|
104
|
-
-
|
|
105
|
-
-
|
|
106
|
-
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
-
|
|
111
|
-
-
|
|
112
|
-
-
|
|
113
|
-
-
|
|
87
|
+
- `task_get.capabilities.available_actions` is the source of truth for v1 executable actions
|
|
88
|
+
- Current public actions are:
|
|
89
|
+
- `approve`
|
|
90
|
+
- `reject`
|
|
91
|
+
- `rollback`
|
|
92
|
+
- `transfer`
|
|
93
|
+
- `urge`
|
|
94
|
+
- Before any approve/reject/rollback/transfer recommendation, always review `task_workflow_log_get` when `task_get.visibility.audit_record_visible=true`
|
|
95
|
+
- If `task_get` returns visible `associated_reports`, review each one with `task_associated_report_detail_get`; do not rely on report summary alone
|
|
96
|
+
- Do not give an approval recommendation based only on `task_get`
|
|
97
|
+
- Do not execute `task_action_execute` until the user explicitly confirms the chosen action
|
|
98
|
+
- Avoid actions on ambiguous tasks or records
|
|
99
|
+
- Summarize the final action and the exact `app_key / record_id / workflow_node_id`
|
|
114
100
|
|
|
115
101
|
## Response Interpretation
|
|
116
102
|
|
|
117
|
-
- `
|
|
118
|
-
- `
|
|
119
|
-
- `
|
|
103
|
+
- `task_list` returns normalized todo rows and is the only default discovery path
|
|
104
|
+
- `task_get` returns node context summary, not full historical report data
|
|
105
|
+
- `task_associated_report_detail_get` may return either:
|
|
106
|
+
- `result_type=view_list`
|
|
107
|
+
- `result_type=chart_data`
|
|
108
|
+
- `task_workflow_log_get` returns workflow log detail only when the node grants log visibility
|
|
120
109
|
- Treat `request_route` as the source of truth for live route debugging
|
|
121
110
|
- If only part of the requested work is completed, explicitly disclose which parts are done and which are not
|
|
122
111
|
|
|
@@ -12,6 +12,7 @@ DEFAULT_USER_AGENT = "qingflow-mcp/1.0"
|
|
|
12
12
|
DEFAULT_RECORD_LIST_TYPE = 8
|
|
13
13
|
ATTACHMENT_QUESTION_TYPE = 13
|
|
14
14
|
DEFAULT_BASE_URL = "https://qingflow.com/api"
|
|
15
|
+
DEFAULT_FEEDBACK_APP_KEY = "e0d017kju002"
|
|
15
16
|
|
|
16
17
|
|
|
17
18
|
def get_mcp_home() -> Path:
|
|
@@ -136,6 +137,43 @@ def get_default_qf_version() -> str | None:
|
|
|
136
137
|
return normalized or None
|
|
137
138
|
|
|
138
139
|
|
|
140
|
+
def get_feedback_qsource_token() -> str | None:
|
|
141
|
+
"""获取反馈 q-source 被动入口 token"""
|
|
142
|
+
value = get_config_value(
|
|
143
|
+
"feedback.qsource_token",
|
|
144
|
+
env_var="QINGFLOW_MCP_FEEDBACK_QSOURCE_TOKEN",
|
|
145
|
+
default=None,
|
|
146
|
+
)
|
|
147
|
+
if value is None:
|
|
148
|
+
return None
|
|
149
|
+
normalized = str(value).strip()
|
|
150
|
+
return normalized or None
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def get_feedback_base_url() -> str | None:
|
|
154
|
+
"""获取反馈 q-source 使用的 base URL"""
|
|
155
|
+
value = get_config_value(
|
|
156
|
+
"feedback.base_url",
|
|
157
|
+
env_var="QINGFLOW_MCP_FEEDBACK_BASE_URL",
|
|
158
|
+
default=None,
|
|
159
|
+
)
|
|
160
|
+
if value is None:
|
|
161
|
+
return get_default_base_url()
|
|
162
|
+
normalized = normalize_base_url(value)
|
|
163
|
+
return normalized or get_default_base_url()
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def get_feedback_app_key() -> str:
|
|
167
|
+
"""获取内部反馈表 app_key"""
|
|
168
|
+
value = get_config_value(
|
|
169
|
+
"feedback.app_key",
|
|
170
|
+
env_var="QINGFLOW_MCP_FEEDBACK_APP_KEY",
|
|
171
|
+
default=DEFAULT_FEEDBACK_APP_KEY,
|
|
172
|
+
)
|
|
173
|
+
normalized = str(value or "").strip()
|
|
174
|
+
return normalized or DEFAULT_FEEDBACK_APP_KEY
|
|
175
|
+
|
|
176
|
+
|
|
139
177
|
def get_timeout_seconds() -> float:
|
|
140
178
|
"""获取 HTTP 超时秒数"""
|
|
141
179
|
value = get_config_value(
|
|
@@ -8,6 +8,7 @@ from .backend_client import BackendClient
|
|
|
8
8
|
from .session_store import SessionStore
|
|
9
9
|
from .tools.app_tools import AppTools
|
|
10
10
|
from .tools.auth_tools import AuthTools
|
|
11
|
+
from .tools.feedback_tools import FeedbackTools
|
|
11
12
|
from .tools.file_tools import FileTools
|
|
12
13
|
from .tools.package_tools import PackageTools
|
|
13
14
|
from .tools.navigation_tools import NavigationTools
|
|
@@ -17,6 +18,7 @@ from .tools.qingbi_report_tools import QingbiReportTools
|
|
|
17
18
|
from .tools.record_tools import RecordTools
|
|
18
19
|
from .tools.role_tools import RoleTools
|
|
19
20
|
from .tools.solution_tools import SolutionTools
|
|
21
|
+
from .tools.task_context_tools import TaskContextTools
|
|
20
22
|
from .tools.view_tools import ViewTools
|
|
21
23
|
from .tools.workflow_tools import WorkflowTools
|
|
22
24
|
from .tools.workspace_tools import WorkspaceTools
|
|
@@ -93,6 +95,14 @@ Analysis answers must include concrete numbers. When applicable, include percent
|
|
|
93
95
|
- If a member or department field id is known but candidate ids are not, use `record_member_candidates` or `record_department_candidates` before `record_write`.
|
|
94
96
|
- For default-all member or department fields, prefer those field candidate tools instead of starting with `directory_*`.
|
|
95
97
|
|
|
98
|
+
## Task Workflow Path
|
|
99
|
+
|
|
100
|
+
`task_list -> task_get -> task_action_execute`
|
|
101
|
+
|
|
102
|
+
- Use `task_associated_report_detail_get` for associated view or report details.
|
|
103
|
+
- Use `task_workflow_log_get` for full workflow log history.
|
|
104
|
+
- Task actions operate on `app_key + record_id + workflow_node_id`, not `task_id`.
|
|
105
|
+
|
|
96
106
|
## Time Handling
|
|
97
107
|
|
|
98
108
|
Normalize relative dates before building DSL.
|
|
@@ -107,14 +117,24 @@ Default to `prod` unless the user explicitly specifies `test`.
|
|
|
107
117
|
|
|
108
118
|
## Constraints
|
|
109
119
|
|
|
110
|
-
Avoid builder-side app or schema changes here.
|
|
120
|
+
Avoid builder-side app or schema changes here.
|
|
121
|
+
|
|
122
|
+
## Feedback Path
|
|
123
|
+
|
|
124
|
+
If the current MCP capability is unsupported, the workflow is awkward, or the user's need still cannot be satisfied after reasonable use, offer to submit product feedback.
|
|
125
|
+
|
|
126
|
+
- First summarize what is still not working
|
|
127
|
+
- Ask the user whether to submit feedback
|
|
128
|
+
- Call `feedback_submit` only after explicit user confirmation""",
|
|
111
129
|
)
|
|
112
130
|
sessions = SessionStore()
|
|
113
131
|
backend = BackendClient()
|
|
114
132
|
AuthTools(sessions, backend).register(server)
|
|
133
|
+
FeedbackTools(backend, mcp_side="通用").register(server)
|
|
115
134
|
WorkspaceTools(sessions, backend).register(server)
|
|
116
135
|
FileTools(sessions, backend).register(server)
|
|
117
136
|
RecordTools(sessions, backend).register(server)
|
|
137
|
+
TaskContextTools(sessions, backend).register(server)
|
|
118
138
|
RoleTools(sessions, backend).register(server)
|
|
119
139
|
AppTools(sessions, backend).register(server)
|
|
120
140
|
QingbiReportTools(sessions, backend).register(server)
|
|
@@ -7,6 +7,7 @@ from .config import DEFAULT_PROFILE
|
|
|
7
7
|
from .session_store import SessionStore
|
|
8
8
|
from .tools.ai_builder_tools import AiBuilderTools
|
|
9
9
|
from .tools.auth_tools import AuthTools
|
|
10
|
+
from .tools.feedback_tools import FeedbackTools
|
|
10
11
|
from .tools.file_tools import FileTools
|
|
11
12
|
from .tools.workspace_tools import WorkspaceTools
|
|
12
13
|
|
|
@@ -26,7 +27,8 @@ def build_builder_server() -> FastMCP:
|
|
|
26
27
|
"Use package_attach_app to attach apps to packages, and app_publish_verify for explicit final publish verification. "
|
|
27
28
|
"For workflow edits, prefer preset plus explicit patching over generating a full custom graph from scratch, and declare node assignees and editable fields explicitly. "
|
|
28
29
|
"If builder writes are blocked by the current user's own edit lock, use app_release_edit_lock_if_mine with the lock owner details from the failed result. "
|
|
29
|
-
"Do not handcraft internal solution payloads or rely on build_id/stage/repair."
|
|
30
|
+
"Do not handcraft internal solution payloads or rely on build_id/stage/repair. "
|
|
31
|
+
"If the current MCP capability is unsupported, the workflow is awkward, or the user's need still cannot be satisfied after reasonable use, first summarize the gap, ask whether to submit feedback, and call feedback_submit only after explicit user confirmation."
|
|
30
32
|
),
|
|
31
33
|
)
|
|
32
34
|
sessions = SessionStore()
|
|
@@ -35,6 +37,7 @@ def build_builder_server() -> FastMCP:
|
|
|
35
37
|
workspace = WorkspaceTools(sessions, backend)
|
|
36
38
|
files = FileTools(sessions, backend)
|
|
37
39
|
ai_builder = AiBuilderTools(sessions, backend)
|
|
40
|
+
feedback = FeedbackTools(backend, mcp_side="App Builder MCP")
|
|
38
41
|
|
|
39
42
|
@server.tool()
|
|
40
43
|
def auth_login(
|
|
@@ -120,6 +123,8 @@ def build_builder_server() -> FastMCP:
|
|
|
120
123
|
file_related_url=file_related_url,
|
|
121
124
|
)
|
|
122
125
|
|
|
126
|
+
feedback.register(server)
|
|
127
|
+
|
|
123
128
|
@server.tool()
|
|
124
129
|
def package_list(profile: str = DEFAULT_PROFILE, trial_status: str = "all") -> dict:
|
|
125
130
|
return ai_builder.package_list(profile=profile, trial_status=trial_status)
|
|
@@ -10,8 +10,10 @@ from .session_store import SessionStore
|
|
|
10
10
|
from .tools.app_tools import AppTools
|
|
11
11
|
from .tools.auth_tools import AuthTools
|
|
12
12
|
from .tools.directory_tools import DirectoryTools
|
|
13
|
+
from .tools.feedback_tools import FeedbackTools
|
|
13
14
|
from .tools.file_tools import FileTools
|
|
14
15
|
from .tools.record_tools import RecordTools
|
|
16
|
+
from .tools.task_context_tools import TaskContextTools
|
|
15
17
|
from .tools.workspace_tools import WorkspaceTools
|
|
16
18
|
|
|
17
19
|
|
|
@@ -81,6 +83,14 @@ Analysis answers must include concrete numbers. When applicable, include percent
|
|
|
81
83
|
- If a member or department field id is known but candidate ids are not, use `record_member_candidates` or `record_department_candidates` before `record_write`.
|
|
82
84
|
- For default-all member or department fields, prefer those field candidate tools instead of starting with `directory_*`.
|
|
83
85
|
|
|
86
|
+
## Task Workflow Path
|
|
87
|
+
|
|
88
|
+
`task_list -> task_get -> task_action_execute`
|
|
89
|
+
|
|
90
|
+
- Use `task_associated_report_detail_get` for associated view or report details.
|
|
91
|
+
- Use `task_workflow_log_get` for full workflow log history.
|
|
92
|
+
- Task actions operate on `app_key + record_id + workflow_node_id`, not `task_id`.
|
|
93
|
+
|
|
84
94
|
## Time Handling
|
|
85
95
|
|
|
86
96
|
Normalize relative dates before building DSL.
|
|
@@ -95,7 +105,15 @@ Default to `prod` unless the user explicitly specifies `test`.
|
|
|
95
105
|
|
|
96
106
|
## Constraints
|
|
97
107
|
|
|
98
|
-
Avoid builder-side app or schema changes here.
|
|
108
|
+
Avoid builder-side app or schema changes here.
|
|
109
|
+
|
|
110
|
+
## Feedback Path
|
|
111
|
+
|
|
112
|
+
If the current MCP capability is unsupported, the workflow is awkward, or the user's need still cannot be satisfied after reasonable use, offer to submit product feedback.
|
|
113
|
+
|
|
114
|
+
- First summarize what is still not working
|
|
115
|
+
- Ask the user whether to submit feedback
|
|
116
|
+
- Call `feedback_submit` only after explicit user confirmation""",
|
|
99
117
|
)
|
|
100
118
|
sessions = SessionStore()
|
|
101
119
|
backend = BackendClient()
|
|
@@ -103,6 +121,7 @@ Avoid builder-side app or schema changes here.""",
|
|
|
103
121
|
apps = AppTools(sessions, backend)
|
|
104
122
|
workspace = WorkspaceTools(sessions, backend)
|
|
105
123
|
files = FileTools(sessions, backend)
|
|
124
|
+
feedback = FeedbackTools(backend, mcp_side="App User MCP")
|
|
106
125
|
|
|
107
126
|
@server.tool()
|
|
108
127
|
def auth_login(
|
|
@@ -218,9 +237,11 @@ Avoid builder-side app or schema changes here.""",
|
|
|
218
237
|
bucket_type=bucket_type,
|
|
219
238
|
path_id=path_id,
|
|
220
239
|
file_related_url=file_related_url,
|
|
221
|
-
|
|
240
|
+
)
|
|
222
241
|
|
|
242
|
+
feedback.register(server)
|
|
223
243
|
RecordTools(sessions, backend).register(server)
|
|
244
|
+
TaskContextTools(sessions, backend).register(server)
|
|
224
245
|
DirectoryTools(sessions, backend).register(server)
|
|
225
246
|
|
|
226
247
|
return server
|
|
@@ -611,9 +611,11 @@ class ApprovalTools(ToolBase):
|
|
|
611
611
|
self._normalize_alias(body, "formId", "form_id")
|
|
612
612
|
|
|
613
613
|
node_id = self._extract_node_id(body)
|
|
614
|
-
body["nodeId"] = node_id
|
|
614
|
+
body["nodeId"] = self._resolve_actionable_node_id(context, app_key, apply_id, node_id)
|
|
615
615
|
body["applyId"] = self._match_or_fill_int(body, field_name="applyId", expected_value=apply_id)
|
|
616
616
|
body["formId"] = self._resolve_form_id(profile, context, app_key, explicit_form_id=body.get("formId"))
|
|
617
|
+
if body.get("answers") is None:
|
|
618
|
+
body["answers"] = self._fetch_current_todo_answers(context, app_key, apply_id, body["nodeId"])
|
|
617
619
|
|
|
618
620
|
self._validate_approval_payload(body)
|
|
619
621
|
return body
|
|
@@ -672,6 +674,54 @@ class ApprovalTools(ToolBase):
|
|
|
672
674
|
elif alias_value is not None and payload.get(canonical_key) != alias_value:
|
|
673
675
|
raise_tool_error(QingflowApiError.config_error(f"payload.{canonical_key} and payload.{alias_key} must match when both are provided"))
|
|
674
676
|
|
|
677
|
+
def _resolve_actionable_node_id(self, context, app_key: str, apply_id: int, node_id: int) -> int: # type: ignore[no-untyped-def]
|
|
678
|
+
infos = self.backend.request(
|
|
679
|
+
"GET",
|
|
680
|
+
context,
|
|
681
|
+
f"/app/{app_key}/apply/{apply_id}/auditInfo",
|
|
682
|
+
params={"type": 1},
|
|
683
|
+
)
|
|
684
|
+
if not isinstance(infos, list) or not infos:
|
|
685
|
+
raise_tool_error(
|
|
686
|
+
QingflowApiError.config_error(
|
|
687
|
+
f"apply_id={apply_id} is not currently actionable for the logged-in user in todo list"
|
|
688
|
+
)
|
|
689
|
+
)
|
|
690
|
+
actionable_node_ids = {
|
|
691
|
+
candidate
|
|
692
|
+
for item in infos
|
|
693
|
+
if isinstance(item, dict)
|
|
694
|
+
for candidate in (item.get("auditNodeId"), item.get("nodeId"))
|
|
695
|
+
if isinstance(candidate, int) and candidate > 0
|
|
696
|
+
}
|
|
697
|
+
if node_id not in actionable_node_ids:
|
|
698
|
+
raise_tool_error(
|
|
699
|
+
QingflowApiError.config_error(
|
|
700
|
+
f"payload.nodeId={node_id} is not an actionable todo node for apply_id={apply_id}"
|
|
701
|
+
)
|
|
702
|
+
)
|
|
703
|
+
return node_id
|
|
704
|
+
|
|
705
|
+
def _fetch_current_todo_answers(self, context, app_key: str, apply_id: int, node_id: int) -> list[dict[str, Any]]: # type: ignore[no-untyped-def]
|
|
706
|
+
detail = self.backend.request(
|
|
707
|
+
"GET",
|
|
708
|
+
context,
|
|
709
|
+
f"/app/{app_key}/apply/{apply_id}",
|
|
710
|
+
params={"role": 3, "listType": 1, "auditNodeId": node_id},
|
|
711
|
+
)
|
|
712
|
+
answers = detail.get("answers") if isinstance(detail, dict) else None
|
|
713
|
+
if not isinstance(answers, list):
|
|
714
|
+
raise_tool_error(
|
|
715
|
+
QingflowApiError.config_error(
|
|
716
|
+
f"cannot resolve current answers for apply_id={apply_id} nodeId={node_id}"
|
|
717
|
+
)
|
|
718
|
+
)
|
|
719
|
+
normalized_answers: list[dict[str, Any]] = []
|
|
720
|
+
for item in answers:
|
|
721
|
+
if isinstance(item, dict):
|
|
722
|
+
normalized_answers.append(dict(item))
|
|
723
|
+
return normalized_answers
|
|
724
|
+
|
|
675
725
|
def _validate_approval_payload(self, payload: dict[str, Any]) -> None:
|
|
676
726
|
self._reject_unsupported_fields(payload)
|
|
677
727
|
if not isinstance(payload.get("formId"), int) or payload["formId"] <= 0:
|
|
@@ -680,6 +730,9 @@ class ApprovalTools(ToolBase):
|
|
|
680
730
|
raise_tool_error(QingflowApiError.config_error("payload.applyId must be a positive integer"))
|
|
681
731
|
if not isinstance(payload.get("nodeId"), int) or payload["nodeId"] <= 0:
|
|
682
732
|
raise_tool_error(QingflowApiError.config_error("payload.nodeId must be a positive integer"))
|
|
733
|
+
answers = payload.get("answers")
|
|
734
|
+
if answers is not None and not isinstance(answers, list):
|
|
735
|
+
raise_tool_error(QingflowApiError.config_error("payload.answers must be an array when provided"))
|
|
683
736
|
|
|
684
737
|
def _validate_audit_payload(self, payload: dict[str, Any], *, require_uid: bool = False) -> None:
|
|
685
738
|
self._reject_unsupported_fields(payload)
|