qingflow-mcp 0.2.0
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/LICENSE +21 -0
- package/README.md +118 -0
- package/dist/qingflow-client.js +255 -0
- package/dist/server.js +889 -0
- package/package.json +47 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# Qingflow MCP (CRUD)
|
|
2
|
+
|
|
3
|
+
This MCP server wraps Qingflow OpenAPI for:
|
|
4
|
+
|
|
5
|
+
- `qf_apps_list`
|
|
6
|
+
- `qf_form_get`
|
|
7
|
+
- `qf_records_list`
|
|
8
|
+
- `qf_record_get`
|
|
9
|
+
- `qf_record_create`
|
|
10
|
+
- `qf_record_update`
|
|
11
|
+
- `qf_operation_get`
|
|
12
|
+
|
|
13
|
+
It intentionally excludes delete for now.
|
|
14
|
+
|
|
15
|
+
## Setup
|
|
16
|
+
|
|
17
|
+
1. Install dependencies:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
2. Set environment variables:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
export QINGFLOW_BASE_URL="https://api.qingflow.com"
|
|
27
|
+
export QINGFLOW_ACCESS_TOKEN="your_access_token"
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Optional:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
export QINGFLOW_FORM_CACHE_TTL_MS=300000
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Run
|
|
37
|
+
|
|
38
|
+
Development:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
npm run dev
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Build and run:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
npm run build
|
|
48
|
+
npm start
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## CLI Install
|
|
52
|
+
|
|
53
|
+
Global install from GitHub:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
npm i -g git+https://github.com/853046310/qingflow-mcp.git
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Or one-click installer:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
curl -fsSL https://raw.githubusercontent.com/853046310/qingflow-mcp/main/install.sh | bash
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Safer (review script before execution):
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
curl -fsSL https://raw.githubusercontent.com/853046310/qingflow-mcp/main/install.sh -o install.sh
|
|
69
|
+
less install.sh
|
|
70
|
+
bash install.sh
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
MCP client config example:
|
|
74
|
+
|
|
75
|
+
```json
|
|
76
|
+
{
|
|
77
|
+
"mcpServers": {
|
|
78
|
+
"qingflow": {
|
|
79
|
+
"command": "qingflow-mcp",
|
|
80
|
+
"env": {
|
|
81
|
+
"QINGFLOW_BASE_URL": "https://api.qingflow.com",
|
|
82
|
+
"QINGFLOW_ACCESS_TOKEN": "your_access_token"
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Recommended Flow
|
|
90
|
+
|
|
91
|
+
1. `qf_apps_list` to pick app.
|
|
92
|
+
2. `qf_form_get` to inspect field ids/titles.
|
|
93
|
+
3. `qf_record_create` or `qf_record_update`.
|
|
94
|
+
4. If create/update returns only `request_id`, call `qf_operation_get` to resolve async result.
|
|
95
|
+
|
|
96
|
+
## Publish
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
npm login
|
|
100
|
+
npm publish
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
If you publish under an npm scope, use:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
npm publish --access public
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Security Notes
|
|
110
|
+
|
|
111
|
+
1. Keep `QINGFLOW_ACCESS_TOKEN` only in runtime env vars; do not commit `.env`.
|
|
112
|
+
2. Rotate token immediately if it appears in screenshots, logs, or chat history.
|
|
113
|
+
|
|
114
|
+
## Community
|
|
115
|
+
|
|
116
|
+
- Contributing: [CONTRIBUTING.md](./CONTRIBUTING.md)
|
|
117
|
+
- Security: [SECURITY.md](./SECURITY.md)
|
|
118
|
+
- Conduct: [CODE_OF_CONDUCT.md](./CODE_OF_CONDUCT.md)
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
export class QingflowApiError extends Error {
|
|
2
|
+
errCode;
|
|
3
|
+
errMsg;
|
|
4
|
+
httpStatus;
|
|
5
|
+
details;
|
|
6
|
+
constructor(params) {
|
|
7
|
+
super(params.message);
|
|
8
|
+
this.name = "QingflowApiError";
|
|
9
|
+
this.errCode = params.errCode ?? null;
|
|
10
|
+
this.errMsg = params.errMsg ?? params.message;
|
|
11
|
+
this.httpStatus = params.httpStatus ?? null;
|
|
12
|
+
this.details = params.details;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export class QingflowClient {
|
|
16
|
+
baseUrl;
|
|
17
|
+
accessToken;
|
|
18
|
+
timeoutMs;
|
|
19
|
+
constructor(config) {
|
|
20
|
+
this.baseUrl = normalizeBaseUrl(config.baseUrl);
|
|
21
|
+
this.accessToken = normalizeAccessToken(config.accessToken);
|
|
22
|
+
this.timeoutMs = config.timeoutMs ?? 30_000;
|
|
23
|
+
}
|
|
24
|
+
listApps(options = {}) {
|
|
25
|
+
return this.request({
|
|
26
|
+
method: "GET",
|
|
27
|
+
path: "/app",
|
|
28
|
+
options: {
|
|
29
|
+
query: {
|
|
30
|
+
userId: options.userId,
|
|
31
|
+
favourite: options.favourite
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
getForm(appKey, options = {}) {
|
|
37
|
+
return this.request({
|
|
38
|
+
method: "GET",
|
|
39
|
+
path: `/app/${encodeURIComponent(appKey)}/form`,
|
|
40
|
+
options: {
|
|
41
|
+
userId: options.userId
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
listRecords(appKey, payload, options = {}) {
|
|
46
|
+
return this.request({
|
|
47
|
+
method: "POST",
|
|
48
|
+
path: `/app/${encodeURIComponent(appKey)}/apply/filter`,
|
|
49
|
+
options: {
|
|
50
|
+
userId: options.userId,
|
|
51
|
+
body: payload
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
getRecord(applyId) {
|
|
56
|
+
return this.request({
|
|
57
|
+
method: "GET",
|
|
58
|
+
path: `/apply/${encodeURIComponent(applyId)}`
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
createRecord(appKey, payload, options = {}) {
|
|
62
|
+
return this.request({
|
|
63
|
+
method: "POST",
|
|
64
|
+
path: `/app/${encodeURIComponent(appKey)}/apply`,
|
|
65
|
+
options: {
|
|
66
|
+
userId: options.userId,
|
|
67
|
+
body: payload
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
updateRecord(applyId, payload, options = {}) {
|
|
72
|
+
return this.request({
|
|
73
|
+
method: "POST",
|
|
74
|
+
path: `/apply/${encodeURIComponent(applyId)}`,
|
|
75
|
+
options: {
|
|
76
|
+
userId: options.userId,
|
|
77
|
+
body: payload
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
getOperation(requestId) {
|
|
82
|
+
return this.request({
|
|
83
|
+
method: "GET",
|
|
84
|
+
path: `/operation/${encodeURIComponent(requestId)}`
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
async request(params) {
|
|
88
|
+
const options = params.options ?? {};
|
|
89
|
+
const url = new URL(params.path, this.baseUrl);
|
|
90
|
+
appendQuery(url, options.query);
|
|
91
|
+
const headers = new Headers();
|
|
92
|
+
headers.set("accessToken", this.accessToken);
|
|
93
|
+
if (options.userId) {
|
|
94
|
+
headers.set("userId", options.userId);
|
|
95
|
+
}
|
|
96
|
+
const init = {
|
|
97
|
+
method: params.method,
|
|
98
|
+
headers
|
|
99
|
+
};
|
|
100
|
+
if (options.body !== undefined) {
|
|
101
|
+
headers.set("content-type", "application/json");
|
|
102
|
+
init.body = JSON.stringify(options.body);
|
|
103
|
+
}
|
|
104
|
+
const controller = new AbortController();
|
|
105
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
106
|
+
init.signal = controller.signal;
|
|
107
|
+
try {
|
|
108
|
+
const response = await fetch(url, init);
|
|
109
|
+
const text = await response.text();
|
|
110
|
+
const data = safeJsonParse(text);
|
|
111
|
+
if (!response.ok) {
|
|
112
|
+
throw new QingflowApiError({
|
|
113
|
+
message: `Qingflow HTTP ${response.status}`,
|
|
114
|
+
httpStatus: response.status,
|
|
115
|
+
errMsg: extractErrMsg(data, text),
|
|
116
|
+
details: data ?? text
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
if (!data || typeof data !== "object") {
|
|
120
|
+
throw new QingflowApiError({
|
|
121
|
+
message: "Qingflow response is not JSON object",
|
|
122
|
+
httpStatus: response.status,
|
|
123
|
+
details: text
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
const parsed = parseResponseEnvelope(data);
|
|
127
|
+
if (parsed.errCode === null) {
|
|
128
|
+
throw new QingflowApiError({
|
|
129
|
+
message: "Qingflow response missing code field",
|
|
130
|
+
httpStatus: response.status,
|
|
131
|
+
details: data
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
if (parsed.errCode !== 0) {
|
|
135
|
+
throw new QingflowApiError({
|
|
136
|
+
message: `Qingflow API error ${parsed.errCode}: ${parsed.errMsg}`,
|
|
137
|
+
errCode: parsed.errCode,
|
|
138
|
+
errMsg: parsed.errMsg,
|
|
139
|
+
httpStatus: response.status,
|
|
140
|
+
details: data
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
return {
|
|
144
|
+
errCode: parsed.errCode,
|
|
145
|
+
errMsg: parsed.errMsg,
|
|
146
|
+
result: parsed.result
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
if (error instanceof QingflowApiError) {
|
|
151
|
+
throw error;
|
|
152
|
+
}
|
|
153
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
154
|
+
throw new QingflowApiError({
|
|
155
|
+
message: `Qingflow request timeout after ${this.timeoutMs}ms`
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
throw new QingflowApiError({
|
|
159
|
+
message: error instanceof Error ? error.message : "Unknown request error",
|
|
160
|
+
details: error
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
finally {
|
|
164
|
+
clearTimeout(timer);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
function normalizeBaseUrl(url) {
|
|
169
|
+
const normalized = url.trim();
|
|
170
|
+
if (!normalized) {
|
|
171
|
+
throw new Error("QINGFLOW_BASE_URL is required");
|
|
172
|
+
}
|
|
173
|
+
return normalized.endsWith("/") ? normalized : `${normalized}/`;
|
|
174
|
+
}
|
|
175
|
+
function normalizeAccessToken(token) {
|
|
176
|
+
const normalized = token.trim();
|
|
177
|
+
if (!normalized) {
|
|
178
|
+
throw new Error("QINGFLOW_ACCESS_TOKEN is required");
|
|
179
|
+
}
|
|
180
|
+
if (/[\r\n]/.test(normalized)) {
|
|
181
|
+
throw new Error("QINGFLOW_ACCESS_TOKEN contains newline characters");
|
|
182
|
+
}
|
|
183
|
+
if (/[\u0100-\uFFFF]/.test(normalized)) {
|
|
184
|
+
throw new Error("QINGFLOW_ACCESS_TOKEN contains non-ASCII characters; please paste raw token only");
|
|
185
|
+
}
|
|
186
|
+
return normalized;
|
|
187
|
+
}
|
|
188
|
+
function appendQuery(url, query) {
|
|
189
|
+
if (!query) {
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
for (const [key, value] of Object.entries(query)) {
|
|
193
|
+
if (value === undefined || value === null) {
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
url.searchParams.set(key, String(value));
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
function safeJsonParse(text) {
|
|
200
|
+
try {
|
|
201
|
+
return JSON.parse(text);
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
function extractErrMsg(json, rawText) {
|
|
208
|
+
if (json && typeof json === "object") {
|
|
209
|
+
const obj = json;
|
|
210
|
+
const msgCandidates = [obj.errMsg, obj.errorMsg, obj.message];
|
|
211
|
+
for (const msg of msgCandidates) {
|
|
212
|
+
if (typeof msg === "string" && msg.trim()) {
|
|
213
|
+
return msg;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
const sample = rawText.trim().slice(0, 200);
|
|
218
|
+
return sample || "request failed";
|
|
219
|
+
}
|
|
220
|
+
function parseResponseEnvelope(data) {
|
|
221
|
+
const codeCandidates = [data.errCode, data.errorCode, data.statusCode];
|
|
222
|
+
let errCode = null;
|
|
223
|
+
for (const candidate of codeCandidates) {
|
|
224
|
+
const parsed = toFiniteNumber(candidate);
|
|
225
|
+
if (parsed !== null) {
|
|
226
|
+
errCode = parsed;
|
|
227
|
+
break;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
const messageCandidates = [data.errMsg, data.errorMsg, data.message];
|
|
231
|
+
let errMsg = "";
|
|
232
|
+
for (const candidate of messageCandidates) {
|
|
233
|
+
if (typeof candidate === "string" && candidate.trim()) {
|
|
234
|
+
errMsg = candidate.trim();
|
|
235
|
+
break;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return {
|
|
239
|
+
errCode,
|
|
240
|
+
errMsg,
|
|
241
|
+
result: data.result
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
function toFiniteNumber(value) {
|
|
245
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
246
|
+
return value;
|
|
247
|
+
}
|
|
248
|
+
if (typeof value === "string" && value.trim()) {
|
|
249
|
+
const parsed = Number(value);
|
|
250
|
+
if (Number.isFinite(parsed)) {
|
|
251
|
+
return parsed;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return null;
|
|
255
|
+
}
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,889 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { QingflowApiError, QingflowClient } from "./qingflow-client.js";
|
|
6
|
+
const MODE_TO_TYPE = {
|
|
7
|
+
todo: 1,
|
|
8
|
+
done: 2,
|
|
9
|
+
mine_approved: 3,
|
|
10
|
+
mine_rejected: 4,
|
|
11
|
+
mine_draft: 5,
|
|
12
|
+
mine_need_improve: 6,
|
|
13
|
+
mine_processing: 7,
|
|
14
|
+
all: 8,
|
|
15
|
+
all_approved: 9,
|
|
16
|
+
all_rejected: 10,
|
|
17
|
+
all_processing: 11,
|
|
18
|
+
cc: 12
|
|
19
|
+
};
|
|
20
|
+
const FORM_CACHE_TTL_MS = Number(process.env.QINGFLOW_FORM_CACHE_TTL_MS ?? "300000");
|
|
21
|
+
const formCache = new Map();
|
|
22
|
+
const accessToken = process.env.QINGFLOW_ACCESS_TOKEN;
|
|
23
|
+
const baseUrl = process.env.QINGFLOW_BASE_URL;
|
|
24
|
+
if (!accessToken) {
|
|
25
|
+
throw new Error("QINGFLOW_ACCESS_TOKEN is required");
|
|
26
|
+
}
|
|
27
|
+
if (!baseUrl) {
|
|
28
|
+
throw new Error("QINGFLOW_BASE_URL is required");
|
|
29
|
+
}
|
|
30
|
+
const client = new QingflowClient({
|
|
31
|
+
accessToken,
|
|
32
|
+
baseUrl
|
|
33
|
+
});
|
|
34
|
+
const server = new McpServer({
|
|
35
|
+
name: "qingflow-mcp",
|
|
36
|
+
version: "0.2.0"
|
|
37
|
+
});
|
|
38
|
+
const jsonPrimitiveSchema = z.union([z.string(), z.number(), z.boolean(), z.null()]);
|
|
39
|
+
const answerValueSchema = z.union([
|
|
40
|
+
jsonPrimitiveSchema,
|
|
41
|
+
z
|
|
42
|
+
.object({
|
|
43
|
+
value: z.unknown().optional(),
|
|
44
|
+
dataValue: z.unknown().optional(),
|
|
45
|
+
id: z.union([z.string(), z.number()]).optional(),
|
|
46
|
+
email: z.string().optional(),
|
|
47
|
+
optionId: z.union([z.string(), z.number()]).optional(),
|
|
48
|
+
otherInfo: z.string().optional(),
|
|
49
|
+
queId: z.union([z.string(), z.number()]).optional(),
|
|
50
|
+
valueStr: z.string().optional(),
|
|
51
|
+
matchValue: z.unknown().optional(),
|
|
52
|
+
ordinal: z.union([z.number(), z.string()]).optional(),
|
|
53
|
+
pluginValue: z.unknown().optional()
|
|
54
|
+
})
|
|
55
|
+
.passthrough()
|
|
56
|
+
]);
|
|
57
|
+
const answerInputSchema = z
|
|
58
|
+
.object({
|
|
59
|
+
que_id: z.union([z.string().min(1), z.number().int()]).optional(),
|
|
60
|
+
queId: z.union([z.string().min(1), z.number().int()]).optional(),
|
|
61
|
+
que_title: z.string().optional(),
|
|
62
|
+
queTitle: z.string().optional(),
|
|
63
|
+
que_type: z.unknown().optional(),
|
|
64
|
+
queType: z.unknown().optional(),
|
|
65
|
+
value: answerValueSchema.optional(),
|
|
66
|
+
values: z.array(answerValueSchema).optional(),
|
|
67
|
+
table_values: z.array(z.array(z.unknown())).optional(),
|
|
68
|
+
tableValues: z.array(z.array(z.unknown())).optional()
|
|
69
|
+
})
|
|
70
|
+
.passthrough()
|
|
71
|
+
.refine((value) => Boolean(value.que_id ?? value.queId), {
|
|
72
|
+
message: "answer item requires que_id or queId"
|
|
73
|
+
})
|
|
74
|
+
.refine((value) => value.value !== undefined ||
|
|
75
|
+
value.values !== undefined ||
|
|
76
|
+
value.table_values !== undefined ||
|
|
77
|
+
value.tableValues !== undefined, {
|
|
78
|
+
message: "answer item requires value(s) or table_values"
|
|
79
|
+
});
|
|
80
|
+
const fieldValueSchema = z.union([
|
|
81
|
+
jsonPrimitiveSchema,
|
|
82
|
+
z.array(z.unknown()),
|
|
83
|
+
z.record(z.unknown())
|
|
84
|
+
]);
|
|
85
|
+
const apiMetaSchema = z.object({
|
|
86
|
+
provider_err_code: z.number(),
|
|
87
|
+
provider_err_msg: z.string().nullable(),
|
|
88
|
+
base_url: z.string()
|
|
89
|
+
});
|
|
90
|
+
const appSchema = z.object({
|
|
91
|
+
appKey: z.string(),
|
|
92
|
+
appName: z.string()
|
|
93
|
+
});
|
|
94
|
+
const fieldSummarySchema = z.object({
|
|
95
|
+
que_id: z.union([z.number(), z.string(), z.null()]),
|
|
96
|
+
que_title: z.string().nullable(),
|
|
97
|
+
que_type: z.unknown(),
|
|
98
|
+
has_sub_fields: z.boolean(),
|
|
99
|
+
sub_field_count: z.number().int().nonnegative()
|
|
100
|
+
});
|
|
101
|
+
const recordItemSchema = z.object({
|
|
102
|
+
apply_id: z.union([z.string(), z.number(), z.null()]),
|
|
103
|
+
app_key: z.string().nullable(),
|
|
104
|
+
apply_num: z.union([z.number(), z.string(), z.null()]),
|
|
105
|
+
apply_time: z.string().nullable(),
|
|
106
|
+
last_update_time: z.string().nullable(),
|
|
107
|
+
answers: z.array(z.unknown()).optional()
|
|
108
|
+
});
|
|
109
|
+
const operationResultSchema = z.object({
|
|
110
|
+
request_id: z.string(),
|
|
111
|
+
operation_result: z.unknown()
|
|
112
|
+
});
|
|
113
|
+
const appsInputSchema = z.object({
|
|
114
|
+
user_id: z.string().min(1).optional(),
|
|
115
|
+
favourite: z.union([z.literal(0), z.literal(1)]).optional(),
|
|
116
|
+
keyword: z.string().min(1).optional(),
|
|
117
|
+
limit: z.number().int().positive().max(500).optional(),
|
|
118
|
+
offset: z.number().int().nonnegative().optional()
|
|
119
|
+
});
|
|
120
|
+
const appsOutputSchema = z.object({
|
|
121
|
+
ok: z.literal(true),
|
|
122
|
+
data: z.object({
|
|
123
|
+
total_apps: z.number().int().nonnegative(),
|
|
124
|
+
returned_apps: z.number().int().nonnegative(),
|
|
125
|
+
limit: z.number().int().positive(),
|
|
126
|
+
offset: z.number().int().nonnegative(),
|
|
127
|
+
apps: z.array(appSchema)
|
|
128
|
+
}),
|
|
129
|
+
meta: apiMetaSchema
|
|
130
|
+
});
|
|
131
|
+
const formInputSchema = z.object({
|
|
132
|
+
app_key: z.string().min(1),
|
|
133
|
+
user_id: z.string().min(1).optional(),
|
|
134
|
+
force_refresh: z.boolean().optional(),
|
|
135
|
+
include_raw: z.boolean().optional()
|
|
136
|
+
});
|
|
137
|
+
const formOutputSchema = z.object({
|
|
138
|
+
ok: z.literal(true),
|
|
139
|
+
data: z.object({
|
|
140
|
+
app_key: z.string(),
|
|
141
|
+
total_fields: z.number().int().nonnegative(),
|
|
142
|
+
field_summaries: z.array(fieldSummarySchema),
|
|
143
|
+
form: z.unknown().optional()
|
|
144
|
+
}),
|
|
145
|
+
meta: apiMetaSchema
|
|
146
|
+
});
|
|
147
|
+
const listInputSchema = z.object({
|
|
148
|
+
app_key: z.string().min(1),
|
|
149
|
+
user_id: z.string().min(1).optional(),
|
|
150
|
+
page_num: z.number().int().positive().optional(),
|
|
151
|
+
page_size: z.number().int().positive().max(200).optional(),
|
|
152
|
+
mode: z
|
|
153
|
+
.enum([
|
|
154
|
+
"todo",
|
|
155
|
+
"done",
|
|
156
|
+
"mine_approved",
|
|
157
|
+
"mine_rejected",
|
|
158
|
+
"mine_draft",
|
|
159
|
+
"mine_need_improve",
|
|
160
|
+
"mine_processing",
|
|
161
|
+
"all",
|
|
162
|
+
"all_approved",
|
|
163
|
+
"all_rejected",
|
|
164
|
+
"all_processing",
|
|
165
|
+
"cc"
|
|
166
|
+
])
|
|
167
|
+
.optional(),
|
|
168
|
+
type: z.number().int().min(1).max(12).optional(),
|
|
169
|
+
keyword: z.string().optional(),
|
|
170
|
+
query_logic: z.enum(["and", "or"]).optional(),
|
|
171
|
+
apply_ids: z.array(z.union([z.string(), z.number()])).optional(),
|
|
172
|
+
sort: z
|
|
173
|
+
.array(z.object({
|
|
174
|
+
que_id: z.union([z.string().min(1), z.number().int()]),
|
|
175
|
+
ascend: z.boolean().optional()
|
|
176
|
+
}))
|
|
177
|
+
.optional(),
|
|
178
|
+
filters: z
|
|
179
|
+
.array(z.object({
|
|
180
|
+
que_id: z.union([z.string().min(1), z.number().int()]).optional(),
|
|
181
|
+
search_key: z.string().optional(),
|
|
182
|
+
search_keys: z.array(z.string()).optional(),
|
|
183
|
+
min_value: z.string().optional(),
|
|
184
|
+
max_value: z.string().optional(),
|
|
185
|
+
scope: z.number().int().optional(),
|
|
186
|
+
search_options: z.array(z.union([z.string(), z.number()])).optional(),
|
|
187
|
+
search_user_ids: z.array(z.string()).optional()
|
|
188
|
+
}))
|
|
189
|
+
.optional(),
|
|
190
|
+
include_answers: z.boolean().optional()
|
|
191
|
+
});
|
|
192
|
+
const listOutputSchema = z.object({
|
|
193
|
+
ok: z.literal(true),
|
|
194
|
+
data: z.object({
|
|
195
|
+
app_key: z.string(),
|
|
196
|
+
pagination: z.object({
|
|
197
|
+
page_num: z.number().int().positive(),
|
|
198
|
+
page_size: z.number().int().positive(),
|
|
199
|
+
page_amount: z.number().int().nonnegative().nullable(),
|
|
200
|
+
result_amount: z.number().int().nonnegative()
|
|
201
|
+
}),
|
|
202
|
+
items: z.array(recordItemSchema)
|
|
203
|
+
}),
|
|
204
|
+
meta: apiMetaSchema
|
|
205
|
+
});
|
|
206
|
+
const recordGetInputSchema = z.object({
|
|
207
|
+
apply_id: z.union([z.string().min(1), z.number().int()])
|
|
208
|
+
});
|
|
209
|
+
const recordGetOutputSchema = z.object({
|
|
210
|
+
ok: z.literal(true),
|
|
211
|
+
data: z.object({
|
|
212
|
+
apply_id: z.union([z.string(), z.number(), z.null()]),
|
|
213
|
+
answer_count: z.number().int().nonnegative(),
|
|
214
|
+
record: z.unknown()
|
|
215
|
+
}),
|
|
216
|
+
meta: apiMetaSchema
|
|
217
|
+
});
|
|
218
|
+
const createInputSchema = z
|
|
219
|
+
.object({
|
|
220
|
+
app_key: z.string().min(1),
|
|
221
|
+
user_id: z.string().min(1).optional(),
|
|
222
|
+
force_refresh_form: z.boolean().optional(),
|
|
223
|
+
apply_user: z
|
|
224
|
+
.object({
|
|
225
|
+
email: z.string().optional(),
|
|
226
|
+
areaCode: z.string().optional(),
|
|
227
|
+
mobile: z.string().optional()
|
|
228
|
+
})
|
|
229
|
+
.passthrough()
|
|
230
|
+
.optional(),
|
|
231
|
+
answers: z.array(answerInputSchema).optional(),
|
|
232
|
+
fields: z.record(fieldValueSchema).optional()
|
|
233
|
+
})
|
|
234
|
+
.refine((value) => hasWritePayload(value.answers, value.fields), {
|
|
235
|
+
message: "Either answers or fields is required"
|
|
236
|
+
});
|
|
237
|
+
const createOutputSchema = z.object({
|
|
238
|
+
ok: z.literal(true),
|
|
239
|
+
data: z.object({
|
|
240
|
+
request_id: z.string().nullable(),
|
|
241
|
+
apply_id: z.union([z.string(), z.number(), z.null()]),
|
|
242
|
+
async_hint: z.string()
|
|
243
|
+
}),
|
|
244
|
+
meta: apiMetaSchema
|
|
245
|
+
});
|
|
246
|
+
const updateInputSchema = z
|
|
247
|
+
.object({
|
|
248
|
+
apply_id: z.union([z.string().min(1), z.number().int()]),
|
|
249
|
+
app_key: z.string().min(1).optional(),
|
|
250
|
+
user_id: z.string().min(1).optional(),
|
|
251
|
+
force_refresh_form: z.boolean().optional(),
|
|
252
|
+
answers: z.array(answerInputSchema).optional(),
|
|
253
|
+
fields: z.record(fieldValueSchema).optional()
|
|
254
|
+
})
|
|
255
|
+
.refine((value) => hasWritePayload(value.answers, value.fields), {
|
|
256
|
+
message: "Either answers or fields is required"
|
|
257
|
+
});
|
|
258
|
+
const updateOutputSchema = z.object({
|
|
259
|
+
ok: z.literal(true),
|
|
260
|
+
data: z.object({
|
|
261
|
+
request_id: z.string().nullable(),
|
|
262
|
+
async_hint: z.string()
|
|
263
|
+
}),
|
|
264
|
+
meta: apiMetaSchema
|
|
265
|
+
});
|
|
266
|
+
const operationInputSchema = z.object({
|
|
267
|
+
request_id: z.string().min(1)
|
|
268
|
+
});
|
|
269
|
+
const operationOutputSchema = z.object({
|
|
270
|
+
ok: z.literal(true),
|
|
271
|
+
data: operationResultSchema,
|
|
272
|
+
meta: apiMetaSchema
|
|
273
|
+
});
|
|
274
|
+
server.registerTool("qf_apps_list", {
|
|
275
|
+
title: "Qingflow Apps List",
|
|
276
|
+
description: "List Qingflow apps with optional filtering and client-side slicing.",
|
|
277
|
+
inputSchema: appsInputSchema,
|
|
278
|
+
outputSchema: appsOutputSchema,
|
|
279
|
+
annotations: {
|
|
280
|
+
readOnlyHint: true,
|
|
281
|
+
idempotentHint: true
|
|
282
|
+
}
|
|
283
|
+
}, async (args) => {
|
|
284
|
+
try {
|
|
285
|
+
const response = await client.listApps({
|
|
286
|
+
userId: args.user_id,
|
|
287
|
+
favourite: args.favourite
|
|
288
|
+
});
|
|
289
|
+
const appList = asArray(asObject(response.result)?.appList)
|
|
290
|
+
.map((item) => asObject(item))
|
|
291
|
+
.filter((item) => Boolean(item))
|
|
292
|
+
.map((item) => ({
|
|
293
|
+
appKey: String(item.appKey ?? ""),
|
|
294
|
+
appName: String(item.appName ?? "")
|
|
295
|
+
}))
|
|
296
|
+
.filter((item) => item.appKey.length > 0);
|
|
297
|
+
const keyword = args.keyword?.trim().toLowerCase();
|
|
298
|
+
const filtered = keyword
|
|
299
|
+
? appList.filter((item) => item.appKey.toLowerCase().includes(keyword) ||
|
|
300
|
+
item.appName.toLowerCase().includes(keyword))
|
|
301
|
+
: appList;
|
|
302
|
+
const offset = args.offset ?? 0;
|
|
303
|
+
const limit = args.limit ?? 50;
|
|
304
|
+
const apps = filtered.slice(offset, offset + limit);
|
|
305
|
+
return okResult({
|
|
306
|
+
ok: true,
|
|
307
|
+
data: {
|
|
308
|
+
total_apps: filtered.length,
|
|
309
|
+
returned_apps: apps.length,
|
|
310
|
+
limit,
|
|
311
|
+
offset,
|
|
312
|
+
apps
|
|
313
|
+
},
|
|
314
|
+
meta: buildMeta(response)
|
|
315
|
+
}, `Returned ${apps.length}/${filtered.length} apps`);
|
|
316
|
+
}
|
|
317
|
+
catch (error) {
|
|
318
|
+
return errorResult(error);
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
server.registerTool("qf_form_get", {
|
|
322
|
+
title: "Qingflow Form Get",
|
|
323
|
+
description: "Get form metadata and compact field summaries for one app.",
|
|
324
|
+
inputSchema: formInputSchema,
|
|
325
|
+
outputSchema: formOutputSchema,
|
|
326
|
+
annotations: {
|
|
327
|
+
readOnlyHint: true,
|
|
328
|
+
idempotentHint: true
|
|
329
|
+
}
|
|
330
|
+
}, async (args) => {
|
|
331
|
+
try {
|
|
332
|
+
const response = await getFormCached(args.app_key, args.user_id, Boolean(args.force_refresh));
|
|
333
|
+
const form = asObject(response.result);
|
|
334
|
+
const fieldSummaries = extractFieldSummaries(form);
|
|
335
|
+
return okResult({
|
|
336
|
+
ok: true,
|
|
337
|
+
data: {
|
|
338
|
+
app_key: args.app_key,
|
|
339
|
+
total_fields: fieldSummaries.length,
|
|
340
|
+
field_summaries: fieldSummaries,
|
|
341
|
+
...(args.include_raw ? { form: response.result } : {})
|
|
342
|
+
},
|
|
343
|
+
meta: buildMeta(response)
|
|
344
|
+
}, `Fetched form for ${args.app_key}`);
|
|
345
|
+
}
|
|
346
|
+
catch (error) {
|
|
347
|
+
return errorResult(error);
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
server.registerTool("qf_records_list", {
|
|
351
|
+
title: "Qingflow Records List",
|
|
352
|
+
description: "List records with pagination, filters and sorting.",
|
|
353
|
+
inputSchema: listInputSchema,
|
|
354
|
+
outputSchema: listOutputSchema,
|
|
355
|
+
annotations: {
|
|
356
|
+
readOnlyHint: true,
|
|
357
|
+
idempotentHint: true
|
|
358
|
+
}
|
|
359
|
+
}, async (args) => {
|
|
360
|
+
try {
|
|
361
|
+
const payload = buildListPayload({
|
|
362
|
+
pageNum: args.page_num ?? 1,
|
|
363
|
+
pageSize: args.page_size ?? 50,
|
|
364
|
+
mode: args.mode,
|
|
365
|
+
type: args.type,
|
|
366
|
+
keyword: args.keyword,
|
|
367
|
+
queryLogic: args.query_logic,
|
|
368
|
+
applyIds: args.apply_ids,
|
|
369
|
+
sort: args.sort,
|
|
370
|
+
filters: args.filters
|
|
371
|
+
});
|
|
372
|
+
const response = await client.listRecords(args.app_key, payload, { userId: args.user_id });
|
|
373
|
+
const result = asObject(response.result);
|
|
374
|
+
const rawItems = asArray(result?.result);
|
|
375
|
+
const includeAnswers = Boolean(args.include_answers);
|
|
376
|
+
const items = rawItems.map((raw) => normalizeRecordItem(raw, includeAnswers));
|
|
377
|
+
return okResult({
|
|
378
|
+
ok: true,
|
|
379
|
+
data: {
|
|
380
|
+
app_key: args.app_key,
|
|
381
|
+
pagination: {
|
|
382
|
+
page_num: toPositiveInt(result?.pageNum) ?? (args.page_num ?? 1),
|
|
383
|
+
page_size: toPositiveInt(result?.pageSize) ?? (args.page_size ?? 50),
|
|
384
|
+
page_amount: toNonNegativeInt(result?.pageAmount),
|
|
385
|
+
result_amount: toNonNegativeInt(result?.resultAmount) ?? items.length
|
|
386
|
+
},
|
|
387
|
+
items
|
|
388
|
+
},
|
|
389
|
+
meta: buildMeta(response)
|
|
390
|
+
}, `Fetched ${items.length} records`);
|
|
391
|
+
}
|
|
392
|
+
catch (error) {
|
|
393
|
+
return errorResult(error);
|
|
394
|
+
}
|
|
395
|
+
});
|
|
396
|
+
server.registerTool("qf_record_get", {
|
|
397
|
+
title: "Qingflow Record Get",
|
|
398
|
+
description: "Get one record by applyId.",
|
|
399
|
+
inputSchema: recordGetInputSchema,
|
|
400
|
+
outputSchema: recordGetOutputSchema,
|
|
401
|
+
annotations: {
|
|
402
|
+
readOnlyHint: true,
|
|
403
|
+
idempotentHint: true
|
|
404
|
+
}
|
|
405
|
+
}, async (args) => {
|
|
406
|
+
try {
|
|
407
|
+
const response = await client.getRecord(String(args.apply_id));
|
|
408
|
+
const record = asObject(response.result) ?? {};
|
|
409
|
+
const answerCount = asArray(record.answers).length;
|
|
410
|
+
return okResult({
|
|
411
|
+
ok: true,
|
|
412
|
+
data: {
|
|
413
|
+
apply_id: record.applyId ?? null,
|
|
414
|
+
answer_count: answerCount,
|
|
415
|
+
record: response.result
|
|
416
|
+
},
|
|
417
|
+
meta: buildMeta(response)
|
|
418
|
+
}, `Fetched record ${String(args.apply_id)}`);
|
|
419
|
+
}
|
|
420
|
+
catch (error) {
|
|
421
|
+
return errorResult(error);
|
|
422
|
+
}
|
|
423
|
+
});
|
|
424
|
+
server.registerTool("qf_record_create", {
|
|
425
|
+
title: "Qingflow Record Create",
|
|
426
|
+
description: "Create one record. Supports explicit answers and ergonomic fields mapping (title or queId).",
|
|
427
|
+
inputSchema: createInputSchema,
|
|
428
|
+
outputSchema: createOutputSchema,
|
|
429
|
+
annotations: {
|
|
430
|
+
readOnlyHint: false,
|
|
431
|
+
idempotentHint: false
|
|
432
|
+
}
|
|
433
|
+
}, async (args) => {
|
|
434
|
+
try {
|
|
435
|
+
const form = needsFormResolution(args.fields) || Boolean(args.force_refresh_form)
|
|
436
|
+
? await getFormCached(args.app_key, args.user_id, Boolean(args.force_refresh_form))
|
|
437
|
+
: null;
|
|
438
|
+
const normalizedAnswers = resolveAnswers({
|
|
439
|
+
explicitAnswers: args.answers,
|
|
440
|
+
fields: args.fields,
|
|
441
|
+
form: form?.result
|
|
442
|
+
});
|
|
443
|
+
const payload = {
|
|
444
|
+
answers: normalizedAnswers
|
|
445
|
+
};
|
|
446
|
+
if (args.apply_user) {
|
|
447
|
+
payload.applyUser = args.apply_user;
|
|
448
|
+
}
|
|
449
|
+
const response = await client.createRecord(args.app_key, payload, {
|
|
450
|
+
userId: args.user_id
|
|
451
|
+
});
|
|
452
|
+
const result = asObject(response.result);
|
|
453
|
+
return okResult({
|
|
454
|
+
ok: true,
|
|
455
|
+
data: {
|
|
456
|
+
request_id: asNullableString(result?.requestId),
|
|
457
|
+
apply_id: result?.applyId ?? null,
|
|
458
|
+
async_hint: "Use qf_operation_get with request_id when apply_id is null."
|
|
459
|
+
},
|
|
460
|
+
meta: buildMeta(response)
|
|
461
|
+
}, `Create request sent for app ${args.app_key}`);
|
|
462
|
+
}
|
|
463
|
+
catch (error) {
|
|
464
|
+
return errorResult(error);
|
|
465
|
+
}
|
|
466
|
+
});
|
|
467
|
+
server.registerTool("qf_record_update", {
|
|
468
|
+
title: "Qingflow Record Update",
|
|
469
|
+
description: "Patch one record by applyId with explicit answers or ergonomic fields mapping.",
|
|
470
|
+
inputSchema: updateInputSchema,
|
|
471
|
+
outputSchema: updateOutputSchema,
|
|
472
|
+
annotations: {
|
|
473
|
+
readOnlyHint: false,
|
|
474
|
+
idempotentHint: false
|
|
475
|
+
}
|
|
476
|
+
}, async (args) => {
|
|
477
|
+
try {
|
|
478
|
+
const requiresForm = needsFormResolution(args.fields);
|
|
479
|
+
if (requiresForm && !args.app_key) {
|
|
480
|
+
throw new Error("app_key is required when fields uses title-based keys");
|
|
481
|
+
}
|
|
482
|
+
const form = requiresForm && args.app_key
|
|
483
|
+
? await getFormCached(args.app_key, args.user_id, Boolean(args.force_refresh_form))
|
|
484
|
+
: null;
|
|
485
|
+
const normalizedAnswers = resolveAnswers({
|
|
486
|
+
explicitAnswers: args.answers,
|
|
487
|
+
fields: args.fields,
|
|
488
|
+
form: form?.result
|
|
489
|
+
});
|
|
490
|
+
const response = await client.updateRecord(String(args.apply_id), { answers: normalizedAnswers }, { userId: args.user_id });
|
|
491
|
+
const result = asObject(response.result);
|
|
492
|
+
return okResult({
|
|
493
|
+
ok: true,
|
|
494
|
+
data: {
|
|
495
|
+
request_id: asNullableString(result?.requestId),
|
|
496
|
+
async_hint: "Use qf_operation_get with request_id to fetch update result when needed."
|
|
497
|
+
},
|
|
498
|
+
meta: buildMeta(response)
|
|
499
|
+
}, `Update request sent for apply ${String(args.apply_id)}`);
|
|
500
|
+
}
|
|
501
|
+
catch (error) {
|
|
502
|
+
return errorResult(error);
|
|
503
|
+
}
|
|
504
|
+
});
|
|
505
|
+
server.registerTool("qf_operation_get", {
|
|
506
|
+
title: "Qingflow Operation Get",
|
|
507
|
+
description: "Resolve async operation result by request_id.",
|
|
508
|
+
inputSchema: operationInputSchema,
|
|
509
|
+
outputSchema: operationOutputSchema,
|
|
510
|
+
annotations: {
|
|
511
|
+
readOnlyHint: true,
|
|
512
|
+
idempotentHint: true
|
|
513
|
+
}
|
|
514
|
+
}, async (args) => {
|
|
515
|
+
try {
|
|
516
|
+
const response = await client.getOperation(args.request_id);
|
|
517
|
+
return okResult({
|
|
518
|
+
ok: true,
|
|
519
|
+
data: {
|
|
520
|
+
request_id: args.request_id,
|
|
521
|
+
operation_result: response.result
|
|
522
|
+
},
|
|
523
|
+
meta: buildMeta(response)
|
|
524
|
+
}, `Resolved operation ${args.request_id}`);
|
|
525
|
+
}
|
|
526
|
+
catch (error) {
|
|
527
|
+
return errorResult(error);
|
|
528
|
+
}
|
|
529
|
+
});
|
|
530
|
+
async function main() {
|
|
531
|
+
const transport = new StdioServerTransport();
|
|
532
|
+
await server.connect(transport);
|
|
533
|
+
}
|
|
534
|
+
void main();
|
|
535
|
+
function hasWritePayload(answers, fields) {
|
|
536
|
+
return Boolean((answers && answers.length > 0) || (fields && Object.keys(fields).length > 0));
|
|
537
|
+
}
|
|
538
|
+
function buildMeta(response) {
|
|
539
|
+
return {
|
|
540
|
+
provider_err_code: response.errCode,
|
|
541
|
+
provider_err_msg: response.errMsg || null,
|
|
542
|
+
base_url: baseUrl
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
function buildListPayload(params) {
|
|
546
|
+
const payload = {
|
|
547
|
+
pageNum: params.pageNum,
|
|
548
|
+
pageSize: params.pageSize
|
|
549
|
+
};
|
|
550
|
+
if (params.mode) {
|
|
551
|
+
payload.type = MODE_TO_TYPE[params.mode];
|
|
552
|
+
}
|
|
553
|
+
else if (params.type !== undefined) {
|
|
554
|
+
payload.type = params.type;
|
|
555
|
+
}
|
|
556
|
+
if (params.keyword) {
|
|
557
|
+
payload.queryKey = params.keyword;
|
|
558
|
+
}
|
|
559
|
+
if (params.queryLogic) {
|
|
560
|
+
payload.queriesRel = params.queryLogic;
|
|
561
|
+
}
|
|
562
|
+
if (params.applyIds?.length) {
|
|
563
|
+
payload.applyIds = params.applyIds.map((id) => String(id));
|
|
564
|
+
}
|
|
565
|
+
if (params.sort?.length) {
|
|
566
|
+
payload.sorts = params.sort.map((item) => ({
|
|
567
|
+
queId: item.que_id,
|
|
568
|
+
...(item.ascend !== undefined ? { isAscend: item.ascend } : {})
|
|
569
|
+
}));
|
|
570
|
+
}
|
|
571
|
+
if (params.filters?.length) {
|
|
572
|
+
payload.queries = params.filters.map((item) => ({
|
|
573
|
+
...(item.que_id !== undefined ? { queId: item.que_id } : {}),
|
|
574
|
+
...(item.search_key !== undefined ? { searchKey: item.search_key } : {}),
|
|
575
|
+
...(item.search_keys !== undefined ? { searchKeys: item.search_keys } : {}),
|
|
576
|
+
...(item.min_value !== undefined ? { minValue: item.min_value } : {}),
|
|
577
|
+
...(item.max_value !== undefined ? { maxValue: item.max_value } : {}),
|
|
578
|
+
...(item.scope !== undefined ? { scope: item.scope } : {}),
|
|
579
|
+
...(item.search_options !== undefined ? { searchOptions: item.search_options } : {}),
|
|
580
|
+
...(item.search_user_ids !== undefined ? { searchUserIds: item.search_user_ids } : {})
|
|
581
|
+
}));
|
|
582
|
+
}
|
|
583
|
+
return payload;
|
|
584
|
+
}
|
|
585
|
+
function normalizeRecordItem(raw, includeAnswers) {
|
|
586
|
+
const item = asObject(raw) ?? {};
|
|
587
|
+
const normalized = {
|
|
588
|
+
apply_id: item.applyId ?? null,
|
|
589
|
+
app_key: asNullableString(item.appKey),
|
|
590
|
+
apply_num: item.applyNum ?? null,
|
|
591
|
+
apply_time: asNullableString(item.applyTime),
|
|
592
|
+
last_update_time: asNullableString(item.lastUpdateTime),
|
|
593
|
+
...(includeAnswers ? { answers: asArray(item.answers) } : {})
|
|
594
|
+
};
|
|
595
|
+
return normalized;
|
|
596
|
+
}
|
|
597
|
+
function resolveAnswers(params) {
|
|
598
|
+
const normalizedFromFields = resolveFieldAnswers(params.fields, params.form);
|
|
599
|
+
const normalizedExplicit = normalizeExplicitAnswers(params.explicitAnswers);
|
|
600
|
+
const merged = new Map();
|
|
601
|
+
for (const answer of normalizedFromFields) {
|
|
602
|
+
merged.set(String(answer.queId), answer);
|
|
603
|
+
}
|
|
604
|
+
for (const answer of normalizedExplicit) {
|
|
605
|
+
merged.set(String(answer.queId), answer);
|
|
606
|
+
}
|
|
607
|
+
if (merged.size === 0) {
|
|
608
|
+
throw new Error("answers or fields must contain at least one field");
|
|
609
|
+
}
|
|
610
|
+
return Array.from(merged.values());
|
|
611
|
+
}
|
|
612
|
+
function normalizeExplicitAnswers(answers) {
|
|
613
|
+
if (!answers?.length) {
|
|
614
|
+
return [];
|
|
615
|
+
}
|
|
616
|
+
const output = [];
|
|
617
|
+
for (const item of answers) {
|
|
618
|
+
const queId = item.que_id ?? item.queId;
|
|
619
|
+
if (queId === undefined || queId === null || String(queId).trim() === "") {
|
|
620
|
+
throw new Error("answer item requires que_id or queId");
|
|
621
|
+
}
|
|
622
|
+
const normalized = {
|
|
623
|
+
queId: isNumericKey(String(queId)) ? Number(queId) : String(queId)
|
|
624
|
+
};
|
|
625
|
+
const queTitle = item.que_title ?? item.queTitle;
|
|
626
|
+
if (typeof queTitle === "string" && queTitle.trim()) {
|
|
627
|
+
normalized.queTitle = queTitle;
|
|
628
|
+
}
|
|
629
|
+
const queType = item.que_type ?? item.queType;
|
|
630
|
+
if (queType !== undefined) {
|
|
631
|
+
normalized.queType = queType;
|
|
632
|
+
}
|
|
633
|
+
const tableValues = item.table_values ?? item.tableValues;
|
|
634
|
+
if (tableValues !== undefined) {
|
|
635
|
+
normalized.tableValues = tableValues;
|
|
636
|
+
output.push(normalized);
|
|
637
|
+
continue;
|
|
638
|
+
}
|
|
639
|
+
const values = item.values ?? (item.value !== undefined ? [item.value] : undefined);
|
|
640
|
+
if (values === undefined) {
|
|
641
|
+
throw new Error(`answer item ${String(queId)} requires values or table_values`);
|
|
642
|
+
}
|
|
643
|
+
normalized.values = values.map((value) => normalizeAnswerValue(value));
|
|
644
|
+
output.push(normalized);
|
|
645
|
+
}
|
|
646
|
+
return output;
|
|
647
|
+
}
|
|
648
|
+
function resolveFieldAnswers(fields, form) {
|
|
649
|
+
const entries = Object.entries(fields ?? {});
|
|
650
|
+
if (entries.length === 0) {
|
|
651
|
+
return [];
|
|
652
|
+
}
|
|
653
|
+
const index = buildFieldIndex(form);
|
|
654
|
+
const answers = [];
|
|
655
|
+
for (const [fieldKey, fieldValue] of entries) {
|
|
656
|
+
const field = resolveFieldByKey(fieldKey, index);
|
|
657
|
+
if (!field) {
|
|
658
|
+
throw new Error(`Cannot resolve field key "${fieldKey}" from form metadata`);
|
|
659
|
+
}
|
|
660
|
+
answers.push(makeAnswerFromField(field, fieldValue));
|
|
661
|
+
}
|
|
662
|
+
return answers;
|
|
663
|
+
}
|
|
664
|
+
function makeAnswerFromField(field, value) {
|
|
665
|
+
const base = {
|
|
666
|
+
queId: field.queId
|
|
667
|
+
};
|
|
668
|
+
if (field.queTitle !== undefined) {
|
|
669
|
+
base.queTitle = field.queTitle;
|
|
670
|
+
}
|
|
671
|
+
if (field.queType !== undefined) {
|
|
672
|
+
base.queType = field.queType;
|
|
673
|
+
}
|
|
674
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
675
|
+
const objectValue = value;
|
|
676
|
+
if ("tableValues" in objectValue || "table_values" in objectValue) {
|
|
677
|
+
return {
|
|
678
|
+
...base,
|
|
679
|
+
tableValues: objectValue.tableValues ?? objectValue.table_values
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
if ("values" in objectValue) {
|
|
683
|
+
return {
|
|
684
|
+
...base,
|
|
685
|
+
values: asArray(objectValue.values).map((item) => normalizeAnswerValue(item))
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
if (Array.isArray(value) && value.length > 0 && Array.isArray(value[0])) {
|
|
690
|
+
return {
|
|
691
|
+
...base,
|
|
692
|
+
tableValues: value
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
const valueArray = Array.isArray(value) ? value : [value];
|
|
696
|
+
return {
|
|
697
|
+
...base,
|
|
698
|
+
values: valueArray.map((item) => normalizeAnswerValue(item))
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
function normalizeAnswerValue(value) {
|
|
702
|
+
if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
703
|
+
return {
|
|
704
|
+
value,
|
|
705
|
+
dataValue: value
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
return value;
|
|
709
|
+
}
|
|
710
|
+
function needsFormResolution(fields) {
|
|
711
|
+
const keys = Object.keys(fields ?? {});
|
|
712
|
+
if (!keys.length) {
|
|
713
|
+
return false;
|
|
714
|
+
}
|
|
715
|
+
return keys.some((key) => !isNumericKey(key));
|
|
716
|
+
}
|
|
717
|
+
async function getFormCached(appKey, userId, forceRefresh = false) {
|
|
718
|
+
const cacheKey = `${appKey}::${userId ?? ""}`;
|
|
719
|
+
if (!forceRefresh) {
|
|
720
|
+
const cached = formCache.get(cacheKey);
|
|
721
|
+
if (cached && cached.expiresAt > Date.now()) {
|
|
722
|
+
return cached.data;
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
const response = await client.getForm(appKey, { userId });
|
|
726
|
+
formCache.set(cacheKey, {
|
|
727
|
+
expiresAt: Date.now() + FORM_CACHE_TTL_MS,
|
|
728
|
+
data: response
|
|
729
|
+
});
|
|
730
|
+
return response;
|
|
731
|
+
}
|
|
732
|
+
function extractFieldSummaries(form) {
|
|
733
|
+
const root = asArray(form?.questionBaseInfos);
|
|
734
|
+
return root.map((raw) => {
|
|
735
|
+
const field = asObject(raw) ?? {};
|
|
736
|
+
const sub = asArray(field.subQuestionBaseInfos);
|
|
737
|
+
return {
|
|
738
|
+
que_id: field.queId ?? null,
|
|
739
|
+
que_title: asNullableString(field.queTitle),
|
|
740
|
+
que_type: field.queType,
|
|
741
|
+
has_sub_fields: sub.length > 0,
|
|
742
|
+
sub_field_count: sub.length
|
|
743
|
+
};
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
function buildFieldIndex(form) {
|
|
747
|
+
const byId = new Map();
|
|
748
|
+
const byTitle = new Map();
|
|
749
|
+
const root = asArray(asObject(form)?.questionBaseInfos);
|
|
750
|
+
const queue = [...root];
|
|
751
|
+
while (queue.length > 0) {
|
|
752
|
+
const current = queue.shift();
|
|
753
|
+
if (!current) {
|
|
754
|
+
continue;
|
|
755
|
+
}
|
|
756
|
+
if (current.queId !== undefined && current.queId !== null) {
|
|
757
|
+
byId.set(String(current.queId), current);
|
|
758
|
+
}
|
|
759
|
+
if (typeof current.queTitle === "string" && current.queTitle.trim()) {
|
|
760
|
+
const titleKey = current.queTitle.trim().toLowerCase();
|
|
761
|
+
const list = byTitle.get(titleKey) ?? [];
|
|
762
|
+
list.push(current);
|
|
763
|
+
byTitle.set(titleKey, list);
|
|
764
|
+
}
|
|
765
|
+
const sub = asArray(current.subQuestionBaseInfos);
|
|
766
|
+
for (const child of sub) {
|
|
767
|
+
queue.push(child);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
return { byId, byTitle };
|
|
771
|
+
}
|
|
772
|
+
function resolveFieldByKey(fieldKey, index) {
|
|
773
|
+
if (isNumericKey(fieldKey)) {
|
|
774
|
+
const normalized = String(Number(fieldKey));
|
|
775
|
+
const hit = index.byId.get(normalized);
|
|
776
|
+
if (hit) {
|
|
777
|
+
return hit;
|
|
778
|
+
}
|
|
779
|
+
return { queId: Number(fieldKey) };
|
|
780
|
+
}
|
|
781
|
+
const titleKey = fieldKey.trim().toLowerCase();
|
|
782
|
+
const matches = index.byTitle.get(titleKey) ?? [];
|
|
783
|
+
if (matches.length === 1) {
|
|
784
|
+
return matches[0];
|
|
785
|
+
}
|
|
786
|
+
if (matches.length > 1) {
|
|
787
|
+
const candidateIds = matches.map((item) => String(item.queId)).join(", ");
|
|
788
|
+
throw new Error(`Field title "${fieldKey}" is ambiguous. Candidate queId: ${candidateIds}`);
|
|
789
|
+
}
|
|
790
|
+
return null;
|
|
791
|
+
}
|
|
792
|
+
function isNumericKey(value) {
|
|
793
|
+
return /^\d+$/.test(value.trim());
|
|
794
|
+
}
|
|
795
|
+
function okResult(payload, message) {
|
|
796
|
+
return {
|
|
797
|
+
structuredContent: payload,
|
|
798
|
+
content: [
|
|
799
|
+
{
|
|
800
|
+
type: "text",
|
|
801
|
+
text: message
|
|
802
|
+
}
|
|
803
|
+
]
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
function errorResult(error) {
|
|
807
|
+
const payload = toErrorPayload(error);
|
|
808
|
+
return {
|
|
809
|
+
isError: true,
|
|
810
|
+
structuredContent: payload,
|
|
811
|
+
content: [
|
|
812
|
+
{
|
|
813
|
+
type: "text",
|
|
814
|
+
text: JSON.stringify(payload, null, 2)
|
|
815
|
+
}
|
|
816
|
+
]
|
|
817
|
+
};
|
|
818
|
+
}
|
|
819
|
+
function toErrorPayload(error) {
|
|
820
|
+
if (error instanceof QingflowApiError) {
|
|
821
|
+
return {
|
|
822
|
+
ok: false,
|
|
823
|
+
message: error.message,
|
|
824
|
+
err_code: error.errCode,
|
|
825
|
+
err_msg: error.errMsg || null,
|
|
826
|
+
http_status: error.httpStatus,
|
|
827
|
+
details: error.details ?? null
|
|
828
|
+
};
|
|
829
|
+
}
|
|
830
|
+
if (error instanceof z.ZodError) {
|
|
831
|
+
return {
|
|
832
|
+
ok: false,
|
|
833
|
+
message: "Invalid arguments",
|
|
834
|
+
issues: error.issues
|
|
835
|
+
};
|
|
836
|
+
}
|
|
837
|
+
if (error instanceof Error) {
|
|
838
|
+
return {
|
|
839
|
+
ok: false,
|
|
840
|
+
message: error.message
|
|
841
|
+
};
|
|
842
|
+
}
|
|
843
|
+
return {
|
|
844
|
+
ok: false,
|
|
845
|
+
message: "Unknown error",
|
|
846
|
+
details: error
|
|
847
|
+
};
|
|
848
|
+
}
|
|
849
|
+
function asObject(value) {
|
|
850
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
851
|
+
? value
|
|
852
|
+
: null;
|
|
853
|
+
}
|
|
854
|
+
function asArray(value) {
|
|
855
|
+
return Array.isArray(value) ? value : [];
|
|
856
|
+
}
|
|
857
|
+
function toPositiveInt(value) {
|
|
858
|
+
if (typeof value === "number" && Number.isInteger(value) && value > 0) {
|
|
859
|
+
return value;
|
|
860
|
+
}
|
|
861
|
+
if (typeof value === "string" && value.trim()) {
|
|
862
|
+
const parsed = Number(value);
|
|
863
|
+
if (Number.isInteger(parsed) && parsed > 0) {
|
|
864
|
+
return parsed;
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
return null;
|
|
868
|
+
}
|
|
869
|
+
function toNonNegativeInt(value) {
|
|
870
|
+
if (typeof value === "number" && Number.isInteger(value) && value >= 0) {
|
|
871
|
+
return value;
|
|
872
|
+
}
|
|
873
|
+
if (typeof value === "string" && value.trim()) {
|
|
874
|
+
const parsed = Number(value);
|
|
875
|
+
if (Number.isInteger(parsed) && parsed >= 0) {
|
|
876
|
+
return parsed;
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
return null;
|
|
880
|
+
}
|
|
881
|
+
function asNullableString(value) {
|
|
882
|
+
if (typeof value === "string") {
|
|
883
|
+
return value;
|
|
884
|
+
}
|
|
885
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
886
|
+
return String(value);
|
|
887
|
+
}
|
|
888
|
+
return null;
|
|
889
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "qingflow-mcp",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"description": "MCP server for Qingflow CRUD workflows",
|
|
8
|
+
"author": "Yanqi Dong",
|
|
9
|
+
"homepage": "https://github.com/853046310/qingflow-mcp#readme",
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/853046310/qingflow-mcp.git"
|
|
13
|
+
},
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/853046310/qingflow-mcp/issues"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"mcp",
|
|
19
|
+
"qingflow",
|
|
20
|
+
"openapi",
|
|
21
|
+
"automation",
|
|
22
|
+
"agent"
|
|
23
|
+
],
|
|
24
|
+
"bin": {
|
|
25
|
+
"qingflow-mcp": "dist/server.js"
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"dist",
|
|
29
|
+
"README.md",
|
|
30
|
+
"LICENSE"
|
|
31
|
+
],
|
|
32
|
+
"scripts": {
|
|
33
|
+
"build": "tsc -p tsconfig.json",
|
|
34
|
+
"dev": "tsx src/server.ts",
|
|
35
|
+
"start": "node dist/server.js",
|
|
36
|
+
"prepublishOnly": "npm run build"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@modelcontextprotocol/sdk": "^1.17.4",
|
|
40
|
+
"zod": "^3.25.76"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@types/node": "^22.18.1",
|
|
44
|
+
"tsx": "^4.20.5",
|
|
45
|
+
"typescript": "^5.9.2"
|
|
46
|
+
}
|
|
47
|
+
}
|