qingflow-mcp 0.2.1 → 0.2.3
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 +45 -2
- package/dist/qingflow-client.js +15 -5
- package/dist/server.js +117 -7
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -14,6 +14,10 @@ It intentionally excludes delete for now.
|
|
|
14
14
|
|
|
15
15
|
## Setup
|
|
16
16
|
|
|
17
|
+
Runtime requirement:
|
|
18
|
+
|
|
19
|
+
- Node.js `>=18`
|
|
20
|
+
|
|
17
21
|
1. Install dependencies:
|
|
18
22
|
|
|
19
23
|
```bash
|
|
@@ -56,6 +60,12 @@ Global install from GitHub:
|
|
|
56
60
|
npm i -g git+https://github.com/853046310/qingflow-mcp.git
|
|
57
61
|
```
|
|
58
62
|
|
|
63
|
+
Install latest from npm:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
npm i -g qingflow-mcp@latest
|
|
67
|
+
```
|
|
68
|
+
|
|
59
69
|
Or one-click installer:
|
|
60
70
|
|
|
61
71
|
```bash
|
|
@@ -97,8 +107,24 @@ MCP client config example:
|
|
|
97
107
|
|
|
98
108
|
1. For `qf_records_list.sort[].que_id`, use a real field `que_id` (numeric) or exact field title from `qf_form_get`.
|
|
99
109
|
2. Avoid aliases like `create_time`; Qingflow often rejects them.
|
|
100
|
-
3.
|
|
101
|
-
4.
|
|
110
|
+
3. Use `max_rows` (or `max_items`) to cap returned rows.
|
|
111
|
+
4. Use `max_columns` to cap answers per row when `include_answers=true`.
|
|
112
|
+
5. Use `select_columns` to return only specific columns (supports `que_id` or exact field title).
|
|
113
|
+
6. When `include_answers=true`, the server still auto-limits by response size to protect MCP context.
|
|
114
|
+
|
|
115
|
+
Example:
|
|
116
|
+
|
|
117
|
+
```json
|
|
118
|
+
{
|
|
119
|
+
"app_key": "your_app_key",
|
|
120
|
+
"mode": "all",
|
|
121
|
+
"page_size": 50,
|
|
122
|
+
"include_answers": true,
|
|
123
|
+
"max_rows": 10,
|
|
124
|
+
"max_columns": 5,
|
|
125
|
+
"select_columns": [1, "客户名称", "1003"]
|
|
126
|
+
}
|
|
127
|
+
```
|
|
102
128
|
|
|
103
129
|
Optional env vars:
|
|
104
130
|
|
|
@@ -107,6 +133,23 @@ export QINGFLOW_LIST_MAX_ITEMS_WITH_ANSWERS=5
|
|
|
107
133
|
export QINGFLOW_LIST_MAX_ITEMS_BYTES=400000
|
|
108
134
|
```
|
|
109
135
|
|
|
136
|
+
## Troubleshooting
|
|
137
|
+
|
|
138
|
+
If you see runtime errors around `Headers` or missing web APIs:
|
|
139
|
+
|
|
140
|
+
1. Upgrade Node to `>=18`.
|
|
141
|
+
2. Upgrade package to latest:
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
npm i -g qingflow-mcp@latest
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
3. Verify runtime:
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
node -e "console.log(process.version, typeof fetch, typeof Headers)"
|
|
151
|
+
```
|
|
152
|
+
|
|
110
153
|
## Publish
|
|
111
154
|
|
|
112
155
|
```bash
|
package/dist/qingflow-client.js
CHANGED
|
@@ -88,24 +88,25 @@ export class QingflowClient {
|
|
|
88
88
|
const options = params.options ?? {};
|
|
89
89
|
const url = new URL(params.path, this.baseUrl);
|
|
90
90
|
appendQuery(url, options.query);
|
|
91
|
-
const headers =
|
|
92
|
-
|
|
91
|
+
const headers = {
|
|
92
|
+
accessToken: this.accessToken
|
|
93
|
+
};
|
|
93
94
|
if (options.userId) {
|
|
94
|
-
headers.
|
|
95
|
+
headers.userId = options.userId;
|
|
95
96
|
}
|
|
96
97
|
const init = {
|
|
97
98
|
method: params.method,
|
|
98
99
|
headers
|
|
99
100
|
};
|
|
100
101
|
if (options.body !== undefined) {
|
|
101
|
-
headers
|
|
102
|
+
headers["content-type"] = "application/json";
|
|
102
103
|
init.body = JSON.stringify(options.body);
|
|
103
104
|
}
|
|
104
105
|
const controller = new AbortController();
|
|
105
106
|
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
106
107
|
init.signal = controller.signal;
|
|
107
108
|
try {
|
|
108
|
-
const response = await
|
|
109
|
+
const response = await getFetch()(url, init);
|
|
109
110
|
const text = await response.text();
|
|
110
111
|
const data = safeJsonParse(text);
|
|
111
112
|
if (!response.ok) {
|
|
@@ -253,3 +254,12 @@ function toFiniteNumber(value) {
|
|
|
253
254
|
}
|
|
254
255
|
return null;
|
|
255
256
|
}
|
|
257
|
+
function getFetch() {
|
|
258
|
+
const runtimeFetch = globalThis.fetch;
|
|
259
|
+
if (typeof runtimeFetch === "function") {
|
|
260
|
+
return runtimeFetch.bind(globalThis);
|
|
261
|
+
}
|
|
262
|
+
throw new QingflowApiError({
|
|
263
|
+
message: "Global fetch is not available. Use Node.js >= 18."
|
|
264
|
+
});
|
|
265
|
+
}
|
package/dist/server.js
CHANGED
|
@@ -190,7 +190,14 @@ const listInputSchema = z.object({
|
|
|
190
190
|
search_user_ids: z.array(z.string()).optional()
|
|
191
191
|
}))
|
|
192
192
|
.optional(),
|
|
193
|
+
max_rows: z.number().int().positive().max(200).optional(),
|
|
193
194
|
max_items: z.number().int().positive().max(200).optional(),
|
|
195
|
+
max_columns: z.number().int().positive().max(200).optional(),
|
|
196
|
+
select_columns: z
|
|
197
|
+
.array(z.union([z.string().min(1), z.number().int()]))
|
|
198
|
+
.min(1)
|
|
199
|
+
.max(200)
|
|
200
|
+
.optional(),
|
|
194
201
|
include_answers: z.boolean().optional()
|
|
195
202
|
});
|
|
196
203
|
const listOutputSchema = z.object({
|
|
@@ -203,7 +210,15 @@ const listOutputSchema = z.object({
|
|
|
203
210
|
page_amount: z.number().int().nonnegative().nullable(),
|
|
204
211
|
result_amount: z.number().int().nonnegative()
|
|
205
212
|
}),
|
|
206
|
-
items: z.array(recordItemSchema)
|
|
213
|
+
items: z.array(recordItemSchema),
|
|
214
|
+
applied_limits: z
|
|
215
|
+
.object({
|
|
216
|
+
include_answers: z.boolean(),
|
|
217
|
+
row_cap: z.number().int().nonnegative(),
|
|
218
|
+
column_cap: z.number().int().positive().nullable(),
|
|
219
|
+
selected_columns: z.array(z.string()).nullable()
|
|
220
|
+
})
|
|
221
|
+
.optional()
|
|
207
222
|
}),
|
|
208
223
|
meta: apiMetaSchema
|
|
209
224
|
});
|
|
@@ -365,6 +380,7 @@ server.registerTool("qf_records_list", {
|
|
|
365
380
|
const pageNum = args.page_num ?? 1;
|
|
366
381
|
const pageSize = args.page_size ?? DEFAULT_PAGE_SIZE;
|
|
367
382
|
const normalizedSort = await normalizeListSort(args.sort, args.app_key, args.user_id);
|
|
383
|
+
const includeAnswers = Boolean(args.include_answers || (args.select_columns && args.select_columns.length > 0));
|
|
368
384
|
const payload = buildListPayload({
|
|
369
385
|
pageNum,
|
|
370
386
|
pageSize,
|
|
@@ -379,20 +395,26 @@ server.registerTool("qf_records_list", {
|
|
|
379
395
|
const response = await client.listRecords(args.app_key, payload, { userId: args.user_id });
|
|
380
396
|
const result = asObject(response.result);
|
|
381
397
|
const rawItems = asArray(result?.result);
|
|
382
|
-
const includeAnswers = Boolean(args.include_answers);
|
|
383
398
|
const listLimit = resolveListItemLimit({
|
|
384
399
|
total: rawItems.length,
|
|
400
|
+
requestedMaxRows: args.max_rows,
|
|
385
401
|
requestedMaxItems: args.max_items,
|
|
386
402
|
includeAnswers
|
|
387
403
|
});
|
|
388
404
|
const items = rawItems
|
|
389
405
|
.slice(0, listLimit.limit)
|
|
390
406
|
.map((raw) => normalizeRecordItem(raw, includeAnswers));
|
|
391
|
-
const
|
|
407
|
+
const columnProjection = projectRecordItemsColumns({
|
|
392
408
|
items,
|
|
409
|
+
includeAnswers,
|
|
410
|
+
maxColumns: args.max_columns,
|
|
411
|
+
selectColumns: args.select_columns
|
|
412
|
+
});
|
|
413
|
+
const fitted = fitListItemsWithinSize({
|
|
414
|
+
items: columnProjection.items,
|
|
393
415
|
limitBytes: MAX_LIST_ITEMS_BYTES
|
|
394
416
|
});
|
|
395
|
-
const truncationReason = mergeTruncationReasons(listLimit.reason, fitted.reason);
|
|
417
|
+
const truncationReason = mergeTruncationReasons(listLimit.reason, columnProjection.reason, fitted.reason);
|
|
396
418
|
return okResult({
|
|
397
419
|
ok: true,
|
|
398
420
|
data: {
|
|
@@ -403,7 +425,13 @@ server.registerTool("qf_records_list", {
|
|
|
403
425
|
page_amount: toNonNegativeInt(result?.pageAmount),
|
|
404
426
|
result_amount: toNonNegativeInt(result?.resultAmount) ?? fitted.items.length
|
|
405
427
|
},
|
|
406
|
-
items: fitted.items
|
|
428
|
+
items: fitted.items,
|
|
429
|
+
applied_limits: {
|
|
430
|
+
include_answers: includeAnswers,
|
|
431
|
+
row_cap: listLimit.limit,
|
|
432
|
+
column_cap: args.max_columns ?? null,
|
|
433
|
+
selected_columns: columnProjection.selectedColumns
|
|
434
|
+
}
|
|
407
435
|
},
|
|
408
436
|
meta: buildMeta(response)
|
|
409
437
|
}, buildRecordsListMessage({
|
|
@@ -841,12 +869,21 @@ function resolveListItemLimit(params) {
|
|
|
841
869
|
if (params.total <= 0) {
|
|
842
870
|
return { limit: 0, reason: null };
|
|
843
871
|
}
|
|
872
|
+
const explicitLimits = [];
|
|
873
|
+
if (params.requestedMaxRows !== undefined) {
|
|
874
|
+
explicitLimits.push({ name: "max_rows", value: params.requestedMaxRows });
|
|
875
|
+
}
|
|
844
876
|
if (params.requestedMaxItems !== undefined) {
|
|
845
|
-
|
|
877
|
+
explicitLimits.push({ name: "max_items", value: params.requestedMaxItems });
|
|
878
|
+
}
|
|
879
|
+
if (explicitLimits.length > 0) {
|
|
880
|
+
const limit = Math.min(params.total, ...explicitLimits.map((item) => item.value));
|
|
846
881
|
if (limit < params.total) {
|
|
847
882
|
return {
|
|
848
883
|
limit,
|
|
849
|
-
reason: `limited by
|
|
884
|
+
reason: `limited by ${explicitLimits
|
|
885
|
+
.map((item) => `${item.name}=${item.value}`)
|
|
886
|
+
.join(", ")} (effective=${limit})`
|
|
850
887
|
};
|
|
851
888
|
}
|
|
852
889
|
return { limit, reason: null };
|
|
@@ -859,6 +896,41 @@ function resolveListItemLimit(params) {
|
|
|
859
896
|
}
|
|
860
897
|
return { limit: params.total, reason: null };
|
|
861
898
|
}
|
|
899
|
+
function projectRecordItemsColumns(params) {
|
|
900
|
+
if (!params.includeAnswers) {
|
|
901
|
+
return {
|
|
902
|
+
items: params.items,
|
|
903
|
+
reason: null,
|
|
904
|
+
selectedColumns: null
|
|
905
|
+
};
|
|
906
|
+
}
|
|
907
|
+
const normalizedSelectors = normalizeColumnSelectors(params.selectColumns);
|
|
908
|
+
const selectorSet = new Set(normalizedSelectors.map((item) => normalizeColumnSelector(item)));
|
|
909
|
+
let columnCapped = false;
|
|
910
|
+
const projectedItems = params.items.map((item) => {
|
|
911
|
+
const answers = asArray(item.answers);
|
|
912
|
+
let projected = answers;
|
|
913
|
+
if (selectorSet.size > 0) {
|
|
914
|
+
projected = answers.filter((answer) => answerMatchesAnySelector(answer, selectorSet));
|
|
915
|
+
}
|
|
916
|
+
if (params.maxColumns !== undefined && projected.length > params.maxColumns) {
|
|
917
|
+
projected = projected.slice(0, params.maxColumns);
|
|
918
|
+
columnCapped = true;
|
|
919
|
+
}
|
|
920
|
+
return {
|
|
921
|
+
...item,
|
|
922
|
+
answers: projected
|
|
923
|
+
};
|
|
924
|
+
});
|
|
925
|
+
const reason = mergeTruncationReasons(selectorSet.size > 0 ? `selected columns=${normalizedSelectors.length}` : null, columnCapped && params.maxColumns !== undefined
|
|
926
|
+
? `limited to max_columns=${params.maxColumns}`
|
|
927
|
+
: null);
|
|
928
|
+
return {
|
|
929
|
+
items: projectedItems,
|
|
930
|
+
reason,
|
|
931
|
+
selectedColumns: normalizedSelectors.length > 0 ? normalizedSelectors : null
|
|
932
|
+
};
|
|
933
|
+
}
|
|
862
934
|
function fitListItemsWithinSize(params) {
|
|
863
935
|
let candidate = params.items;
|
|
864
936
|
let size = jsonSizeBytes(candidate);
|
|
@@ -887,6 +959,44 @@ function buildRecordsListMessage(params) {
|
|
|
887
959
|
}
|
|
888
960
|
return `Fetched ${params.returned}/${params.total} records (${params.truncationReason})`;
|
|
889
961
|
}
|
|
962
|
+
function normalizeColumnSelectors(selectColumns) {
|
|
963
|
+
if (!selectColumns?.length) {
|
|
964
|
+
return [];
|
|
965
|
+
}
|
|
966
|
+
const deduped = new Set();
|
|
967
|
+
for (const value of selectColumns) {
|
|
968
|
+
const normalized = String(value).trim();
|
|
969
|
+
if (!normalized) {
|
|
970
|
+
continue;
|
|
971
|
+
}
|
|
972
|
+
deduped.add(normalized);
|
|
973
|
+
}
|
|
974
|
+
return Array.from(deduped);
|
|
975
|
+
}
|
|
976
|
+
function normalizeColumnSelector(value) {
|
|
977
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
978
|
+
return `id:${Math.trunc(value)}`;
|
|
979
|
+
}
|
|
980
|
+
const normalized = String(value).trim();
|
|
981
|
+
if (!normalized) {
|
|
982
|
+
return "title:";
|
|
983
|
+
}
|
|
984
|
+
if (isNumericKey(normalized)) {
|
|
985
|
+
return `id:${Number(normalized)}`;
|
|
986
|
+
}
|
|
987
|
+
return `title:${normalized.toLowerCase()}`;
|
|
988
|
+
}
|
|
989
|
+
function answerMatchesAnySelector(answer, selectorSet) {
|
|
990
|
+
const obj = asObject(answer);
|
|
991
|
+
if (!obj) {
|
|
992
|
+
return false;
|
|
993
|
+
}
|
|
994
|
+
const candidates = [
|
|
995
|
+
normalizeColumnSelector(asNullableString(obj.queId) ?? ""),
|
|
996
|
+
normalizeColumnSelector(asNullableString(obj.queTitle) ?? "")
|
|
997
|
+
];
|
|
998
|
+
return candidates.some((candidate) => selectorSet.has(candidate));
|
|
999
|
+
}
|
|
890
1000
|
function normalizeQueId(queId) {
|
|
891
1001
|
if (typeof queId === "number" && Number.isInteger(queId)) {
|
|
892
1002
|
return queId;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "qingflow-mcp",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"private": false,
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -35,6 +35,9 @@
|
|
|
35
35
|
"start": "node dist/server.js",
|
|
36
36
|
"prepublishOnly": "npm run build"
|
|
37
37
|
},
|
|
38
|
+
"engines": {
|
|
39
|
+
"node": ">=18"
|
|
40
|
+
},
|
|
38
41
|
"dependencies": {
|
|
39
42
|
"@modelcontextprotocol/sdk": "^1.17.4",
|
|
40
43
|
"zod": "^3.25.76"
|