qingflow-mcp 0.3.2 → 0.3.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 +4 -2
- package/dist/server.js +102 -43
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -37,6 +37,8 @@ Optional:
|
|
|
37
37
|
|
|
38
38
|
```bash
|
|
39
39
|
export QINGFLOW_FORM_CACHE_TTL_MS=300000
|
|
40
|
+
export QINGFLOW_REQUEST_TIMEOUT_MS=18000
|
|
41
|
+
export QINGFLOW_EXECUTION_BUDGET_MS=20000
|
|
40
42
|
```
|
|
41
43
|
|
|
42
44
|
## Run
|
|
@@ -247,8 +249,8 @@ Aggregate example (`qf_records_aggregate`):
|
|
|
247
249
|
"app_key": "your_app_key",
|
|
248
250
|
"group_by": ["归属部门", "归属销售"],
|
|
249
251
|
"amount_column": "报价总金额",
|
|
250
|
-
"requested_pages":
|
|
251
|
-
"scan_max_pages":
|
|
252
|
+
"requested_pages": 10,
|
|
253
|
+
"scan_max_pages": 10,
|
|
252
254
|
"strict_full": true
|
|
253
255
|
}
|
|
254
256
|
```
|
package/dist/server.js
CHANGED
|
@@ -44,9 +44,11 @@ class InputValidationError extends Error {
|
|
|
44
44
|
const FORM_CACHE_TTL_MS = Number(process.env.QINGFLOW_FORM_CACHE_TTL_MS ?? "300000");
|
|
45
45
|
const formCache = new Map();
|
|
46
46
|
const DEFAULT_PAGE_SIZE = 50;
|
|
47
|
-
const DEFAULT_SCAN_MAX_PAGES =
|
|
47
|
+
const DEFAULT_SCAN_MAX_PAGES = 10;
|
|
48
48
|
const DEFAULT_ROW_LIMIT = 200;
|
|
49
49
|
const MAX_LIST_ITEMS_BYTES = toPositiveInt(process.env.QINGFLOW_LIST_MAX_ITEMS_BYTES) ?? 400000;
|
|
50
|
+
const REQUEST_TIMEOUT_MS = toPositiveInt(process.env.QINGFLOW_REQUEST_TIMEOUT_MS) ?? 18000;
|
|
51
|
+
const EXECUTION_BUDGET_MS = toPositiveInt(process.env.QINGFLOW_EXECUTION_BUDGET_MS) ?? 20000;
|
|
50
52
|
const accessToken = process.env.QINGFLOW_ACCESS_TOKEN;
|
|
51
53
|
const baseUrl = process.env.QINGFLOW_BASE_URL;
|
|
52
54
|
if (!accessToken) {
|
|
@@ -57,11 +59,12 @@ if (!baseUrl) {
|
|
|
57
59
|
}
|
|
58
60
|
const client = new QingflowClient({
|
|
59
61
|
accessToken,
|
|
60
|
-
baseUrl
|
|
62
|
+
baseUrl,
|
|
63
|
+
timeoutMs: REQUEST_TIMEOUT_MS
|
|
61
64
|
});
|
|
62
65
|
const server = new McpServer({
|
|
63
66
|
name: "qingflow-mcp",
|
|
64
|
-
version: "0.3.
|
|
67
|
+
version: "0.3.3"
|
|
65
68
|
});
|
|
66
69
|
const jsonPrimitiveSchema = z.union([z.string(), z.number(), z.boolean(), z.null()]);
|
|
67
70
|
const answerValueSchema = z.union([
|
|
@@ -1226,9 +1229,10 @@ function missingRequiredFieldError(params) {
|
|
|
1226
1229
|
});
|
|
1227
1230
|
}
|
|
1228
1231
|
function normalizeListInput(raw) {
|
|
1229
|
-
const
|
|
1232
|
+
const parsedRoot = parseJsonLikeDeep(raw);
|
|
1233
|
+
const obj = asObject(parsedRoot);
|
|
1230
1234
|
if (!obj) {
|
|
1231
|
-
return
|
|
1235
|
+
return parsedRoot;
|
|
1232
1236
|
}
|
|
1233
1237
|
return {
|
|
1234
1238
|
...obj,
|
|
@@ -1250,9 +1254,10 @@ function normalizeListInput(raw) {
|
|
|
1250
1254
|
};
|
|
1251
1255
|
}
|
|
1252
1256
|
function normalizeRecordGetInput(raw) {
|
|
1253
|
-
const
|
|
1257
|
+
const parsedRoot = parseJsonLikeDeep(raw);
|
|
1258
|
+
const obj = asObject(parsedRoot);
|
|
1254
1259
|
if (!obj) {
|
|
1255
|
-
return
|
|
1260
|
+
return parsedRoot;
|
|
1256
1261
|
}
|
|
1257
1262
|
return {
|
|
1258
1263
|
...obj,
|
|
@@ -1262,9 +1267,10 @@ function normalizeRecordGetInput(raw) {
|
|
|
1262
1267
|
};
|
|
1263
1268
|
}
|
|
1264
1269
|
function normalizeQueryInput(raw) {
|
|
1265
|
-
const
|
|
1270
|
+
const parsedRoot = parseJsonLikeDeep(raw);
|
|
1271
|
+
const obj = asObject(parsedRoot);
|
|
1266
1272
|
if (!obj) {
|
|
1267
|
-
return
|
|
1273
|
+
return parsedRoot;
|
|
1268
1274
|
}
|
|
1269
1275
|
return {
|
|
1270
1276
|
...obj,
|
|
@@ -1287,9 +1293,10 @@ function normalizeQueryInput(raw) {
|
|
|
1287
1293
|
};
|
|
1288
1294
|
}
|
|
1289
1295
|
function normalizeAggregateInput(raw) {
|
|
1290
|
-
const
|
|
1296
|
+
const parsedRoot = parseJsonLikeDeep(raw);
|
|
1297
|
+
const obj = asObject(parsedRoot);
|
|
1291
1298
|
if (!obj) {
|
|
1292
|
-
return
|
|
1299
|
+
return parsedRoot;
|
|
1293
1300
|
}
|
|
1294
1301
|
return {
|
|
1295
1302
|
...obj,
|
|
@@ -1309,8 +1316,12 @@ function normalizeAggregateInput(raw) {
|
|
|
1309
1316
|
};
|
|
1310
1317
|
}
|
|
1311
1318
|
function coerceNumberLike(value) {
|
|
1312
|
-
|
|
1313
|
-
|
|
1319
|
+
const parsed = parseJsonLikeDeep(value);
|
|
1320
|
+
if (typeof parsed === "number" && Number.isFinite(parsed)) {
|
|
1321
|
+
return parsed;
|
|
1322
|
+
}
|
|
1323
|
+
if (typeof parsed === "string") {
|
|
1324
|
+
const trimmed = parsed.trim();
|
|
1314
1325
|
if (/^-?\d+(\.\d+)?$/.test(trimmed)) {
|
|
1315
1326
|
const parsed = Number(trimmed);
|
|
1316
1327
|
if (Number.isFinite(parsed)) {
|
|
@@ -1318,11 +1329,15 @@ function coerceNumberLike(value) {
|
|
|
1318
1329
|
}
|
|
1319
1330
|
}
|
|
1320
1331
|
}
|
|
1321
|
-
return
|
|
1332
|
+
return parsed;
|
|
1322
1333
|
}
|
|
1323
1334
|
function coerceBooleanLike(value) {
|
|
1324
|
-
|
|
1325
|
-
|
|
1335
|
+
const parsed = parseJsonLikeDeep(value);
|
|
1336
|
+
if (typeof parsed === "boolean") {
|
|
1337
|
+
return parsed;
|
|
1338
|
+
}
|
|
1339
|
+
if (typeof parsed === "string") {
|
|
1340
|
+
const trimmed = parsed.trim().toLowerCase();
|
|
1326
1341
|
if (trimmed === "true") {
|
|
1327
1342
|
return true;
|
|
1328
1343
|
}
|
|
@@ -1330,28 +1345,41 @@ function coerceBooleanLike(value) {
|
|
|
1330
1345
|
return false;
|
|
1331
1346
|
}
|
|
1332
1347
|
}
|
|
1333
|
-
return
|
|
1348
|
+
return parsed;
|
|
1334
1349
|
}
|
|
1335
|
-
function
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1350
|
+
function parseJsonLikeDeep(value, maxDepth = 4) {
|
|
1351
|
+
let current = value;
|
|
1352
|
+
for (let i = 0; i < maxDepth; i += 1) {
|
|
1353
|
+
if (typeof current !== "string") {
|
|
1354
|
+
return current;
|
|
1355
|
+
}
|
|
1356
|
+
const trimmed = current.trim();
|
|
1357
|
+
if (!trimmed) {
|
|
1358
|
+
return current;
|
|
1359
|
+
}
|
|
1360
|
+
const singleQuoted = trimmed.startsWith("'") && trimmed.endsWith("'") && trimmed.length >= 2;
|
|
1361
|
+
const candidate = singleQuoted ? trimmed.slice(1, -1) : trimmed;
|
|
1362
|
+
const shouldTryJson = candidate.startsWith("{") ||
|
|
1363
|
+
candidate.startsWith("[") ||
|
|
1364
|
+
(candidate.startsWith('"') && candidate.endsWith('"'));
|
|
1365
|
+
if (!shouldTryJson) {
|
|
1366
|
+
return current;
|
|
1367
|
+
}
|
|
1368
|
+
try {
|
|
1369
|
+
const parsed = JSON.parse(candidate);
|
|
1370
|
+
if (Object.is(parsed, current)) {
|
|
1371
|
+
return current;
|
|
1372
|
+
}
|
|
1373
|
+
current = parsed;
|
|
1374
|
+
}
|
|
1375
|
+
catch {
|
|
1376
|
+
return current;
|
|
1377
|
+
}
|
|
1351
1378
|
}
|
|
1379
|
+
return current;
|
|
1352
1380
|
}
|
|
1353
1381
|
function normalizeSelectorListInput(value) {
|
|
1354
|
-
const parsed =
|
|
1382
|
+
const parsed = parseJsonLikeDeep(value);
|
|
1355
1383
|
if (Array.isArray(parsed)) {
|
|
1356
1384
|
return parsed.map((item) => coerceNumberLike(item));
|
|
1357
1385
|
}
|
|
@@ -1375,7 +1403,7 @@ function normalizeSelectorListInput(value) {
|
|
|
1375
1403
|
return parsed;
|
|
1376
1404
|
}
|
|
1377
1405
|
function normalizeIdArrayInput(value) {
|
|
1378
|
-
const parsed =
|
|
1406
|
+
const parsed = parseJsonLikeDeep(value);
|
|
1379
1407
|
if (Array.isArray(parsed)) {
|
|
1380
1408
|
return parsed.map((item) => coerceNumberLike(item));
|
|
1381
1409
|
}
|
|
@@ -1389,7 +1417,7 @@ function normalizeIdArrayInput(value) {
|
|
|
1389
1417
|
return parsed;
|
|
1390
1418
|
}
|
|
1391
1419
|
function normalizeSortInput(value) {
|
|
1392
|
-
const parsed =
|
|
1420
|
+
const parsed = parseJsonLikeDeep(value);
|
|
1393
1421
|
if (!Array.isArray(parsed)) {
|
|
1394
1422
|
return parsed;
|
|
1395
1423
|
}
|
|
@@ -1406,7 +1434,7 @@ function normalizeSortInput(value) {
|
|
|
1406
1434
|
});
|
|
1407
1435
|
}
|
|
1408
1436
|
function normalizeFiltersInput(value) {
|
|
1409
|
-
const parsed =
|
|
1437
|
+
const parsed = parseJsonLikeDeep(value);
|
|
1410
1438
|
if (parsed === undefined || parsed === null) {
|
|
1411
1439
|
return parsed;
|
|
1412
1440
|
}
|
|
@@ -1425,7 +1453,7 @@ function normalizeFiltersInput(value) {
|
|
|
1425
1453
|
});
|
|
1426
1454
|
}
|
|
1427
1455
|
function normalizeTimeRangeInput(value) {
|
|
1428
|
-
const parsed =
|
|
1456
|
+
const parsed = parseJsonLikeDeep(value);
|
|
1429
1457
|
const obj = asObject(parsed);
|
|
1430
1458
|
if (!obj) {
|
|
1431
1459
|
return parsed;
|
|
@@ -1470,6 +1498,9 @@ function decodeContinuationToken(token) {
|
|
|
1470
1498
|
page_size: pageSize
|
|
1471
1499
|
};
|
|
1472
1500
|
}
|
|
1501
|
+
function isExecutionBudgetExceeded(startedAt) {
|
|
1502
|
+
return Date.now() - startedAt >= EXECUTION_BUDGET_MS;
|
|
1503
|
+
}
|
|
1473
1504
|
function buildEvidencePayload(state, sourcePages) {
|
|
1474
1505
|
return {
|
|
1475
1506
|
query_id: state.query_id,
|
|
@@ -1619,6 +1650,7 @@ async function executeRecordsList(args) {
|
|
|
1619
1650
|
const effectiveFilters = appendTimeRangeFilter(args.filters, args.time_range);
|
|
1620
1651
|
const normalizedSort = await normalizeListSort(args.sort, args.app_key, args.user_id);
|
|
1621
1652
|
const includeAnswers = true;
|
|
1653
|
+
const startedAt = Date.now();
|
|
1622
1654
|
let currentPage = pageNum;
|
|
1623
1655
|
let fetchedPages = 0;
|
|
1624
1656
|
let hasMore = false;
|
|
@@ -1629,6 +1661,11 @@ async function executeRecordsList(args) {
|
|
|
1629
1661
|
const sourcePages = [];
|
|
1630
1662
|
const collectedRawItems = [];
|
|
1631
1663
|
while (fetchedPages < requestedPages && fetchedPages < scanMaxPages) {
|
|
1664
|
+
if (fetchedPages > 0 && isExecutionBudgetExceeded(startedAt)) {
|
|
1665
|
+
hasMore = true;
|
|
1666
|
+
nextPageNum = currentPage;
|
|
1667
|
+
break;
|
|
1668
|
+
}
|
|
1632
1669
|
const payload = buildListPayload({
|
|
1633
1670
|
pageNum: currentPage,
|
|
1634
1671
|
pageSize,
|
|
@@ -1675,9 +1712,16 @@ async function executeRecordsList(args) {
|
|
|
1675
1712
|
selectColumns: args.select_columns
|
|
1676
1713
|
});
|
|
1677
1714
|
if (items.length > 0 && columnProjection.matchedAnswersCount === 0) {
|
|
1678
|
-
throw new
|
|
1679
|
-
|
|
1680
|
-
|
|
1715
|
+
throw new InputValidationError({
|
|
1716
|
+
message: `No answers matched select_columns (${args.select_columns
|
|
1717
|
+
.map((item) => String(item))
|
|
1718
|
+
.join(", ")}).`,
|
|
1719
|
+
errorCode: "COLUMN_SELECTOR_NOT_FOUND",
|
|
1720
|
+
fixHint: "Use qf_form_get to confirm que_id/que_title. If parameters were stringified, pass native JSON arrays (or plain arrays) for select_columns.",
|
|
1721
|
+
details: {
|
|
1722
|
+
select_columns: args.select_columns
|
|
1723
|
+
}
|
|
1724
|
+
});
|
|
1681
1725
|
}
|
|
1682
1726
|
const fitted = fitListItemsWithinSize({
|
|
1683
1727
|
items: columnProjection.items,
|
|
@@ -1904,6 +1948,7 @@ async function executeRecordsSummary(args) {
|
|
|
1904
1948
|
: null
|
|
1905
1949
|
};
|
|
1906
1950
|
let currentPage = startPage;
|
|
1951
|
+
const startedAt = Date.now();
|
|
1907
1952
|
let scannedPages = 0;
|
|
1908
1953
|
let scannedRecords = 0;
|
|
1909
1954
|
let hasMore = false;
|
|
@@ -1916,6 +1961,11 @@ async function executeRecordsSummary(args) {
|
|
|
1916
1961
|
const rows = [];
|
|
1917
1962
|
const byDay = new Map();
|
|
1918
1963
|
while (scannedPages < requestedPages && scannedPages < scanMaxPages) {
|
|
1964
|
+
if (scannedPages > 0 && isExecutionBudgetExceeded(startedAt)) {
|
|
1965
|
+
hasMore = true;
|
|
1966
|
+
nextPageNum = currentPage;
|
|
1967
|
+
break;
|
|
1968
|
+
}
|
|
1919
1969
|
const payload = buildListPayload({
|
|
1920
1970
|
pageNum: currentPage,
|
|
1921
1971
|
pageSize,
|
|
@@ -2138,6 +2188,7 @@ async function executeRecordsAggregate(args) {
|
|
|
2138
2188
|
: null
|
|
2139
2189
|
};
|
|
2140
2190
|
let currentPage = startPage;
|
|
2191
|
+
const startedAt = Date.now();
|
|
2141
2192
|
let scannedPages = 0;
|
|
2142
2193
|
let scannedRecords = 0;
|
|
2143
2194
|
let hasMore = false;
|
|
@@ -2148,6 +2199,11 @@ async function executeRecordsAggregate(args) {
|
|
|
2148
2199
|
const sourcePages = [];
|
|
2149
2200
|
const groupStats = new Map();
|
|
2150
2201
|
while (scannedPages < requestedPages && scannedPages < scanMaxPages) {
|
|
2202
|
+
if (scannedPages > 0 && isExecutionBudgetExceeded(startedAt)) {
|
|
2203
|
+
hasMore = true;
|
|
2204
|
+
nextPageNum = currentPage;
|
|
2205
|
+
break;
|
|
2206
|
+
}
|
|
2151
2207
|
const payload = buildListPayload({
|
|
2152
2208
|
pageNum: currentPage,
|
|
2153
2209
|
pageSize,
|
|
@@ -2981,14 +3037,17 @@ function toErrorPayload(error) {
|
|
|
2981
3037
|
};
|
|
2982
3038
|
}
|
|
2983
3039
|
if (error instanceof QingflowApiError) {
|
|
3040
|
+
const timeoutHint = /timeout/i.test(error.message) || /timeout/i.test(error.errMsg);
|
|
2984
3041
|
return {
|
|
2985
3042
|
ok: false,
|
|
2986
|
-
error_code: "QINGFLOW_API_ERROR",
|
|
3043
|
+
error_code: timeoutHint ? "UPSTREAM_TIMEOUT" : "QINGFLOW_API_ERROR",
|
|
2987
3044
|
message: error.message,
|
|
2988
3045
|
err_code: error.errCode,
|
|
2989
3046
|
err_msg: error.errMsg || null,
|
|
2990
3047
|
http_status: error.httpStatus,
|
|
2991
|
-
fix_hint:
|
|
3048
|
+
fix_hint: timeoutHint
|
|
3049
|
+
? "Upstream request timed out. Reduce page_size/requested_pages, narrow filters, or continue with next_page_token."
|
|
3050
|
+
: "Check app_key/accessToken and request body against qf_form_get field definitions.",
|
|
2992
3051
|
next_page_token: null,
|
|
2993
3052
|
details: error.details ?? null
|
|
2994
3053
|
};
|