qingflow-mcp 0.3.2 → 0.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +4 -2
  2. package/dist/server.js +341 -103
  3. 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": 50,
251
- "scan_max_pages": 50,
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 = 50;
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.1"
67
+ version: "0.3.4"
65
68
  });
66
69
  const jsonPrimitiveSchema = z.union([z.string(), z.number(), z.boolean(), z.null()]);
67
70
  const answerValueSchema = z.union([
@@ -1225,92 +1228,149 @@ function missingRequiredFieldError(params) {
1225
1228
  }
1226
1229
  });
1227
1230
  }
1231
+ const COMMON_INPUT_ALIASES = {
1232
+ appKey: "app_key",
1233
+ userId: "user_id",
1234
+ pageNum: "page_num",
1235
+ pageSize: "page_size",
1236
+ pageToken: "page_token",
1237
+ requestedPages: "requested_pages",
1238
+ scanMaxPages: "scan_max_pages",
1239
+ queryMode: "query_mode",
1240
+ queryLogic: "query_logic",
1241
+ applyId: "apply_id",
1242
+ applyIds: "apply_ids",
1243
+ maxRows: "max_rows",
1244
+ maxItems: "max_items",
1245
+ maxColumns: "max_columns",
1246
+ selectColumns: "select_columns",
1247
+ keepColumns: "keep_columns",
1248
+ keep_columns: "select_columns",
1249
+ includeAnswers: "include_answers",
1250
+ amountColumn: "amount_column",
1251
+ amountColumns: "amount_column",
1252
+ amount_columns: "amount_column",
1253
+ timeRange: "time_range",
1254
+ statPolicy: "stat_policy",
1255
+ groupBy: "group_by",
1256
+ strictFull: "strict_full",
1257
+ forceRefresh: "force_refresh",
1258
+ forceRefreshForm: "force_refresh_form",
1259
+ applyUser: "apply_user"
1260
+ };
1261
+ function applyAliases(obj, aliases) {
1262
+ const out = { ...obj };
1263
+ for (const [alias, canonical] of Object.entries(aliases)) {
1264
+ if (out[canonical] === undefined && out[alias] !== undefined) {
1265
+ out[canonical] = out[alias];
1266
+ }
1267
+ }
1268
+ return out;
1269
+ }
1228
1270
  function normalizeListInput(raw) {
1229
- const obj = asObject(raw);
1271
+ const parsedRoot = parseJsonLikeDeep(raw);
1272
+ const obj = asObject(parsedRoot);
1230
1273
  if (!obj) {
1231
- return raw;
1274
+ return parsedRoot;
1232
1275
  }
1276
+ const normalizedObj = applyAliases(obj, COMMON_INPUT_ALIASES);
1277
+ const selectColumns = normalizedObj.select_columns ?? normalizedObj.keep_columns;
1233
1278
  return {
1234
- ...obj,
1235
- page_num: coerceNumberLike(obj.page_num),
1236
- page_size: coerceNumberLike(obj.page_size),
1237
- requested_pages: coerceNumberLike(obj.requested_pages),
1238
- scan_max_pages: coerceNumberLike(obj.scan_max_pages),
1239
- type: coerceNumberLike(obj.type),
1240
- max_rows: coerceNumberLike(obj.max_rows),
1241
- max_items: coerceNumberLike(obj.max_items),
1242
- max_columns: coerceNumberLike(obj.max_columns),
1243
- strict_full: coerceBooleanLike(obj.strict_full),
1244
- include_answers: coerceBooleanLike(obj.include_answers),
1245
- apply_ids: normalizeIdArrayInput(obj.apply_ids),
1246
- sort: normalizeSortInput(obj.sort),
1247
- filters: normalizeFiltersInput(obj.filters),
1248
- select_columns: normalizeSelectorListInput(obj.select_columns),
1249
- time_range: normalizeTimeRangeInput(obj.time_range)
1279
+ ...normalizedObj,
1280
+ page_num: coerceNumberLike(normalizedObj.page_num),
1281
+ page_size: coerceNumberLike(normalizedObj.page_size),
1282
+ requested_pages: coerceNumberLike(normalizedObj.requested_pages),
1283
+ scan_max_pages: coerceNumberLike(normalizedObj.scan_max_pages),
1284
+ type: coerceNumberLike(normalizedObj.type),
1285
+ max_rows: coerceNumberLike(normalizedObj.max_rows),
1286
+ max_items: coerceNumberLike(normalizedObj.max_items),
1287
+ max_columns: coerceNumberLike(normalizedObj.max_columns),
1288
+ strict_full: coerceBooleanLike(normalizedObj.strict_full),
1289
+ include_answers: coerceBooleanLike(normalizedObj.include_answers),
1290
+ apply_ids: normalizeIdArrayInput(normalizedObj.apply_ids),
1291
+ sort: normalizeSortInput(normalizedObj.sort),
1292
+ filters: normalizeFiltersInput(normalizedObj.filters),
1293
+ select_columns: normalizeSelectorListInput(selectColumns),
1294
+ time_range: normalizeTimeRangeInput(normalizedObj.time_range)
1250
1295
  };
1251
1296
  }
1252
1297
  function normalizeRecordGetInput(raw) {
1253
- const obj = asObject(raw);
1298
+ const parsedRoot = parseJsonLikeDeep(raw);
1299
+ const obj = asObject(parsedRoot);
1254
1300
  if (!obj) {
1255
- return raw;
1301
+ return parsedRoot;
1256
1302
  }
1303
+ const normalizedObj = applyAliases(obj, COMMON_INPUT_ALIASES);
1304
+ const selectColumns = normalizedObj.select_columns ?? normalizedObj.keep_columns;
1257
1305
  return {
1258
- ...obj,
1259
- apply_id: coerceNumberLike(obj.apply_id),
1260
- max_columns: coerceNumberLike(obj.max_columns),
1261
- select_columns: normalizeSelectorListInput(obj.select_columns)
1306
+ ...normalizedObj,
1307
+ apply_id: coerceNumberLike(normalizedObj.apply_id),
1308
+ max_columns: coerceNumberLike(normalizedObj.max_columns),
1309
+ select_columns: normalizeSelectorListInput(selectColumns)
1262
1310
  };
1263
1311
  }
1264
1312
  function normalizeQueryInput(raw) {
1265
- const obj = asObject(raw);
1313
+ const parsedRoot = parseJsonLikeDeep(raw);
1314
+ const obj = asObject(parsedRoot);
1266
1315
  if (!obj) {
1267
- return raw;
1316
+ return parsedRoot;
1268
1317
  }
1318
+ const normalizedObj = applyAliases(obj, COMMON_INPUT_ALIASES);
1319
+ const selectColumns = normalizedObj.select_columns ?? normalizedObj.keep_columns;
1269
1320
  return {
1270
- ...obj,
1271
- page_num: coerceNumberLike(obj.page_num),
1272
- page_size: coerceNumberLike(obj.page_size),
1273
- requested_pages: coerceNumberLike(obj.requested_pages),
1274
- scan_max_pages: coerceNumberLike(obj.scan_max_pages),
1275
- type: coerceNumberLike(obj.type),
1276
- max_rows: coerceNumberLike(obj.max_rows),
1277
- max_items: coerceNumberLike(obj.max_items),
1278
- max_columns: coerceNumberLike(obj.max_columns),
1279
- apply_id: coerceNumberLike(obj.apply_id),
1280
- strict_full: coerceBooleanLike(obj.strict_full),
1281
- include_answers: coerceBooleanLike(obj.include_answers),
1282
- apply_ids: normalizeIdArrayInput(obj.apply_ids),
1283
- sort: normalizeSortInput(obj.sort),
1284
- filters: normalizeFiltersInput(obj.filters),
1285
- select_columns: normalizeSelectorListInput(obj.select_columns),
1286
- time_range: normalizeTimeRangeInput(obj.time_range)
1321
+ ...normalizedObj,
1322
+ page_num: coerceNumberLike(normalizedObj.page_num),
1323
+ page_size: coerceNumberLike(normalizedObj.page_size),
1324
+ requested_pages: coerceNumberLike(normalizedObj.requested_pages),
1325
+ scan_max_pages: coerceNumberLike(normalizedObj.scan_max_pages),
1326
+ type: coerceNumberLike(normalizedObj.type),
1327
+ max_rows: coerceNumberLike(normalizedObj.max_rows),
1328
+ max_items: coerceNumberLike(normalizedObj.max_items),
1329
+ max_columns: coerceNumberLike(normalizedObj.max_columns),
1330
+ apply_id: coerceNumberLike(normalizedObj.apply_id),
1331
+ strict_full: coerceBooleanLike(normalizedObj.strict_full),
1332
+ include_answers: coerceBooleanLike(normalizedObj.include_answers),
1333
+ amount_column: coerceNumberLike(normalizedObj.amount_column),
1334
+ apply_ids: normalizeIdArrayInput(normalizedObj.apply_ids),
1335
+ sort: normalizeSortInput(normalizedObj.sort),
1336
+ filters: normalizeFiltersInput(normalizedObj.filters),
1337
+ select_columns: normalizeSelectorListInput(selectColumns),
1338
+ time_range: normalizeTimeRangeInput(normalizedObj.time_range),
1339
+ stat_policy: normalizeStatPolicyInput(normalizedObj.stat_policy)
1287
1340
  };
1288
1341
  }
1289
1342
  function normalizeAggregateInput(raw) {
1290
- const obj = asObject(raw);
1343
+ const parsedRoot = parseJsonLikeDeep(raw);
1344
+ const obj = asObject(parsedRoot);
1291
1345
  if (!obj) {
1292
- return raw;
1346
+ return parsedRoot;
1293
1347
  }
1348
+ const normalizedObj = applyAliases(obj, COMMON_INPUT_ALIASES);
1294
1349
  return {
1295
- ...obj,
1296
- page_num: coerceNumberLike(obj.page_num),
1297
- page_size: coerceNumberLike(obj.page_size),
1298
- requested_pages: coerceNumberLike(obj.requested_pages),
1299
- scan_max_pages: coerceNumberLike(obj.scan_max_pages),
1300
- type: coerceNumberLike(obj.type),
1301
- max_groups: coerceNumberLike(obj.max_groups),
1302
- strict_full: coerceBooleanLike(obj.strict_full),
1303
- group_by: normalizeSelectorListInput(obj.group_by),
1304
- amount_column: coerceNumberLike(obj.amount_column),
1305
- apply_ids: normalizeIdArrayInput(obj.apply_ids),
1306
- sort: normalizeSortInput(obj.sort),
1307
- filters: normalizeFiltersInput(obj.filters),
1308
- time_range: normalizeTimeRangeInput(obj.time_range)
1350
+ ...normalizedObj,
1351
+ page_num: coerceNumberLike(normalizedObj.page_num),
1352
+ page_size: coerceNumberLike(normalizedObj.page_size),
1353
+ requested_pages: coerceNumberLike(normalizedObj.requested_pages),
1354
+ scan_max_pages: coerceNumberLike(normalizedObj.scan_max_pages),
1355
+ type: coerceNumberLike(normalizedObj.type),
1356
+ max_groups: coerceNumberLike(normalizedObj.max_groups),
1357
+ strict_full: coerceBooleanLike(normalizedObj.strict_full),
1358
+ group_by: normalizeSelectorListInput(normalizedObj.group_by),
1359
+ amount_column: coerceNumberLike(normalizedObj.amount_column),
1360
+ apply_ids: normalizeIdArrayInput(normalizedObj.apply_ids),
1361
+ sort: normalizeSortInput(normalizedObj.sort),
1362
+ filters: normalizeFiltersInput(normalizedObj.filters),
1363
+ time_range: normalizeTimeRangeInput(normalizedObj.time_range),
1364
+ stat_policy: normalizeStatPolicyInput(normalizedObj.stat_policy)
1309
1365
  };
1310
1366
  }
1311
1367
  function coerceNumberLike(value) {
1312
- if (typeof value === "string") {
1313
- const trimmed = value.trim();
1368
+ const parsed = parseJsonLikeDeep(value);
1369
+ if (typeof parsed === "number" && Number.isFinite(parsed)) {
1370
+ return parsed;
1371
+ }
1372
+ if (typeof parsed === "string") {
1373
+ const trimmed = parsed.trim();
1314
1374
  if (/^-?\d+(\.\d+)?$/.test(trimmed)) {
1315
1375
  const parsed = Number(trimmed);
1316
1376
  if (Number.isFinite(parsed)) {
@@ -1318,11 +1378,15 @@ function coerceNumberLike(value) {
1318
1378
  }
1319
1379
  }
1320
1380
  }
1321
- return value;
1381
+ return parsed;
1322
1382
  }
1323
1383
  function coerceBooleanLike(value) {
1324
- if (typeof value === "string") {
1325
- const trimmed = value.trim().toLowerCase();
1384
+ const parsed = parseJsonLikeDeep(value);
1385
+ if (typeof parsed === "boolean") {
1386
+ return parsed;
1387
+ }
1388
+ if (typeof parsed === "string") {
1389
+ const trimmed = parsed.trim().toLowerCase();
1326
1390
  if (trimmed === "true") {
1327
1391
  return true;
1328
1392
  }
@@ -1330,28 +1394,41 @@ function coerceBooleanLike(value) {
1330
1394
  return false;
1331
1395
  }
1332
1396
  }
1333
- return value;
1397
+ return parsed;
1334
1398
  }
1335
- function parseJsonLike(value) {
1336
- if (typeof value !== "string") {
1337
- return value;
1338
- }
1339
- const trimmed = value.trim();
1340
- if (!trimmed) {
1341
- return value;
1342
- }
1343
- if (!trimmed.startsWith("[") && !trimmed.startsWith("{")) {
1344
- return value;
1345
- }
1346
- try {
1347
- return JSON.parse(trimmed);
1348
- }
1349
- catch {
1350
- return value;
1399
+ function parseJsonLikeDeep(value, maxDepth = 4) {
1400
+ let current = value;
1401
+ for (let i = 0; i < maxDepth; i += 1) {
1402
+ if (typeof current !== "string") {
1403
+ return current;
1404
+ }
1405
+ const trimmed = current.trim();
1406
+ if (!trimmed) {
1407
+ return current;
1408
+ }
1409
+ const singleQuoted = trimmed.startsWith("'") && trimmed.endsWith("'") && trimmed.length >= 2;
1410
+ const candidate = singleQuoted ? trimmed.slice(1, -1) : trimmed;
1411
+ const shouldTryJson = candidate.startsWith("{") ||
1412
+ candidate.startsWith("[") ||
1413
+ (candidate.startsWith('"') && candidate.endsWith('"'));
1414
+ if (!shouldTryJson) {
1415
+ return current;
1416
+ }
1417
+ try {
1418
+ const parsed = JSON.parse(candidate);
1419
+ if (Object.is(parsed, current)) {
1420
+ return current;
1421
+ }
1422
+ current = parsed;
1423
+ }
1424
+ catch {
1425
+ return current;
1426
+ }
1351
1427
  }
1428
+ return current;
1352
1429
  }
1353
1430
  function normalizeSelectorListInput(value) {
1354
- const parsed = parseJsonLike(value);
1431
+ const parsed = parseJsonLikeDeep(value);
1355
1432
  if (Array.isArray(parsed)) {
1356
1433
  return parsed.map((item) => coerceNumberLike(item));
1357
1434
  }
@@ -1375,7 +1452,7 @@ function normalizeSelectorListInput(value) {
1375
1452
  return parsed;
1376
1453
  }
1377
1454
  function normalizeIdArrayInput(value) {
1378
- const parsed = parseJsonLike(value);
1455
+ const parsed = parseJsonLikeDeep(value);
1379
1456
  if (Array.isArray(parsed)) {
1380
1457
  return parsed.map((item) => coerceNumberLike(item));
1381
1458
  }
@@ -1389,7 +1466,7 @@ function normalizeIdArrayInput(value) {
1389
1466
  return parsed;
1390
1467
  }
1391
1468
  function normalizeSortInput(value) {
1392
- const parsed = parseJsonLike(value);
1469
+ const parsed = parseJsonLikeDeep(value);
1393
1470
  if (!Array.isArray(parsed)) {
1394
1471
  return parsed;
1395
1472
  }
@@ -1398,15 +1475,16 @@ function normalizeSortInput(value) {
1398
1475
  if (!obj) {
1399
1476
  return item;
1400
1477
  }
1478
+ const normalizedObj = applyAliases(obj, { queId: "que_id", isAscend: "ascend" });
1401
1479
  return {
1402
- ...obj,
1403
- que_id: coerceNumberLike(obj.que_id),
1404
- ascend: coerceBooleanLike(obj.ascend)
1480
+ ...normalizedObj,
1481
+ que_id: coerceNumberLike(normalizedObj.que_id),
1482
+ ascend: coerceBooleanLike(normalizedObj.ascend)
1405
1483
  };
1406
1484
  });
1407
1485
  }
1408
1486
  function normalizeFiltersInput(value) {
1409
- const parsed = parseJsonLike(value);
1487
+ const parsed = parseJsonLikeDeep(value);
1410
1488
  if (parsed === undefined || parsed === null) {
1411
1489
  return parsed;
1412
1490
  }
@@ -1416,23 +1494,53 @@ function normalizeFiltersInput(value) {
1416
1494
  if (!obj) {
1417
1495
  return item;
1418
1496
  }
1497
+ const normalizedObj = applyAliases(obj, {
1498
+ queId: "que_id",
1499
+ searchKey: "search_key",
1500
+ searchKeys: "search_keys",
1501
+ minValue: "min_value",
1502
+ maxValue: "max_value",
1503
+ searchOptions: "search_options",
1504
+ searchUserIds: "search_user_ids"
1505
+ });
1419
1506
  return {
1420
- ...obj,
1421
- que_id: coerceNumberLike(obj.que_id),
1422
- scope: coerceNumberLike(obj.scope),
1423
- search_options: normalizeIdArrayInput(obj.search_options)
1507
+ ...normalizedObj,
1508
+ que_id: coerceNumberLike(normalizedObj.que_id),
1509
+ scope: coerceNumberLike(normalizedObj.scope),
1510
+ search_options: normalizeIdArrayInput(normalizedObj.search_options)
1424
1511
  };
1425
1512
  });
1426
1513
  }
1427
1514
  function normalizeTimeRangeInput(value) {
1428
- const parsed = parseJsonLike(value);
1515
+ const parsed = parseJsonLikeDeep(value);
1516
+ const obj = asObject(parsed);
1517
+ if (!obj) {
1518
+ return parsed;
1519
+ }
1520
+ const normalizedObj = applyAliases(obj, {
1521
+ queId: "column",
1522
+ que_id: "column",
1523
+ timeZone: "timezone"
1524
+ });
1525
+ return {
1526
+ ...normalizedObj,
1527
+ column: coerceNumberLike(normalizedObj.column)
1528
+ };
1529
+ }
1530
+ function normalizeStatPolicyInput(value) {
1531
+ const parsed = parseJsonLikeDeep(value);
1429
1532
  const obj = asObject(parsed);
1430
1533
  if (!obj) {
1431
1534
  return parsed;
1432
1535
  }
1536
+ const normalizedObj = applyAliases(obj, {
1537
+ includeNegative: "include_negative",
1538
+ includeNull: "include_null"
1539
+ });
1433
1540
  return {
1434
- ...obj,
1435
- column: coerceNumberLike(obj.column)
1541
+ ...normalizedObj,
1542
+ include_negative: coerceBooleanLike(normalizedObj.include_negative),
1543
+ include_null: coerceBooleanLike(normalizedObj.include_null)
1436
1544
  };
1437
1545
  }
1438
1546
  function resolveStartPage(pageNum, pageToken, appKey) {
@@ -1470,6 +1578,9 @@ function decodeContinuationToken(token) {
1470
1578
  page_size: pageSize
1471
1579
  };
1472
1580
  }
1581
+ function isExecutionBudgetExceeded(startedAt) {
1582
+ return Date.now() - startedAt >= EXECUTION_BUDGET_MS;
1583
+ }
1473
1584
  function buildEvidencePayload(state, sourcePages) {
1474
1585
  return {
1475
1586
  query_id: state.query_id,
@@ -1575,6 +1686,98 @@ function appendTimeRangeFilter(inputFilters, timeRange) {
1575
1686
  }
1576
1687
  return filters.length > 0 ? filters : undefined;
1577
1688
  }
1689
+ function isLikelyDateLiteral(value) {
1690
+ if (!value) {
1691
+ return false;
1692
+ }
1693
+ const trimmed = value.trim();
1694
+ if (!trimmed) {
1695
+ return false;
1696
+ }
1697
+ return /^\d{4}-\d{2}-\d{2}(?:[ T]\d{2}:\d{2}:\d{2})?$/.test(trimmed);
1698
+ }
1699
+ function isDateLikeQueType(queType) {
1700
+ if (typeof queType === "number" && Number.isInteger(queType)) {
1701
+ return queType === 4;
1702
+ }
1703
+ if (typeof queType === "string") {
1704
+ const trimmed = queType.trim().toLowerCase();
1705
+ if (trimmed === "date" || trimmed === "datetime") {
1706
+ return true;
1707
+ }
1708
+ if (/^\d+$/.test(trimmed)) {
1709
+ return Number(trimmed) === 4;
1710
+ }
1711
+ }
1712
+ return false;
1713
+ }
1714
+ function pickDateFieldSuggestion(index) {
1715
+ for (const field of index.byId.values()) {
1716
+ if (!isDateLikeQueType(field.queType) || field.queId === undefined || field.queId === null) {
1717
+ continue;
1718
+ }
1719
+ const normalizedQueId = normalizeQueId(field.queId);
1720
+ if (typeof normalizedQueId === "number" && normalizedQueId <= 0) {
1721
+ continue;
1722
+ }
1723
+ return {
1724
+ que_id: normalizedQueId,
1725
+ que_title: asNullableString(field.queTitle)
1726
+ };
1727
+ }
1728
+ return null;
1729
+ }
1730
+ function hasDateLikeRangeFilters(filters) {
1731
+ return (filters ?? []).some((item) => item.que_id !== undefined &&
1732
+ (isLikelyDateLiteral(item.min_value) || isLikelyDateLiteral(item.max_value)));
1733
+ }
1734
+ function validateDateRangeFilters(filters, index, tool) {
1735
+ for (const filter of filters ?? []) {
1736
+ if (filter.que_id === undefined ||
1737
+ (!isLikelyDateLiteral(filter.min_value) && !isLikelyDateLiteral(filter.max_value))) {
1738
+ continue;
1739
+ }
1740
+ let resolved;
1741
+ try {
1742
+ resolved = resolveFieldByKey(String(filter.que_id), index);
1743
+ }
1744
+ catch (error) {
1745
+ throw new InputValidationError({
1746
+ message: `Cannot resolve filter field "${String(filter.que_id)}"`,
1747
+ errorCode: "INVALID_FILTER_FIELD",
1748
+ fixHint: "Use qf_form_get to confirm exact que_id/que_title before passing filters.",
1749
+ details: {
1750
+ tool,
1751
+ filter,
1752
+ reason: error instanceof Error ? error.message : String(error)
1753
+ }
1754
+ });
1755
+ }
1756
+ if (!resolved || resolved.queType === undefined || resolved.queType === null) {
1757
+ continue;
1758
+ }
1759
+ if (isDateLikeQueType(resolved.queType)) {
1760
+ continue;
1761
+ }
1762
+ const suggestion = pickDateFieldSuggestion(index);
1763
+ throw new InputValidationError({
1764
+ message: `Date-like filter range targets non-date field "${String(filter.que_id)}"`,
1765
+ errorCode: "FILTER_FIELD_TYPE_MISMATCH",
1766
+ fixHint: suggestion
1767
+ ? `Use a date field for date range filters, e.g. que_id=${String(suggestion.que_id)} (${suggestion.que_title ?? "date field"}).`
1768
+ : "Use a date field (queType=4) for date range filters.",
1769
+ details: {
1770
+ tool,
1771
+ filter,
1772
+ resolved_field: {
1773
+ que_id: resolved.queId ?? null,
1774
+ que_title: asNullableString(resolved.queTitle),
1775
+ que_type: resolved.queType
1776
+ }
1777
+ }
1778
+ });
1779
+ }
1780
+ }
1578
1781
  function buildRecordGetArgsFromQuery(args) {
1579
1782
  if (args.apply_id === undefined) {
1580
1783
  throw missingRequiredFieldError({
@@ -1617,8 +1820,14 @@ async function executeRecordsList(args) {
1617
1820
  const requestedPages = args.requested_pages ?? 1;
1618
1821
  const scanMaxPages = args.scan_max_pages ?? requestedPages;
1619
1822
  const effectiveFilters = appendTimeRangeFilter(args.filters, args.time_range);
1823
+ if (hasDateLikeRangeFilters(effectiveFilters)) {
1824
+ const form = await getFormCached(args.app_key, args.user_id, false);
1825
+ const index = buildFieldIndex(form.result);
1826
+ validateDateRangeFilters(effectiveFilters, index, "qf_records_list");
1827
+ }
1620
1828
  const normalizedSort = await normalizeListSort(args.sort, args.app_key, args.user_id);
1621
1829
  const includeAnswers = true;
1830
+ const startedAt = Date.now();
1622
1831
  let currentPage = pageNum;
1623
1832
  let fetchedPages = 0;
1624
1833
  let hasMore = false;
@@ -1629,6 +1838,11 @@ async function executeRecordsList(args) {
1629
1838
  const sourcePages = [];
1630
1839
  const collectedRawItems = [];
1631
1840
  while (fetchedPages < requestedPages && fetchedPages < scanMaxPages) {
1841
+ if (fetchedPages > 0 && isExecutionBudgetExceeded(startedAt)) {
1842
+ hasMore = true;
1843
+ nextPageNum = currentPage;
1844
+ break;
1845
+ }
1632
1846
  const payload = buildListPayload({
1633
1847
  pageNum: currentPage,
1634
1848
  pageSize,
@@ -1675,9 +1889,16 @@ async function executeRecordsList(args) {
1675
1889
  selectColumns: args.select_columns
1676
1890
  });
1677
1891
  if (items.length > 0 && columnProjection.matchedAnswersCount === 0) {
1678
- throw new Error(`No answers matched select_columns (${args.select_columns
1679
- .map((item) => String(item))
1680
- .join(", ")}). Check que_id/title from qf_form_get.`);
1892
+ throw new InputValidationError({
1893
+ message: `No answers matched select_columns (${args.select_columns
1894
+ .map((item) => String(item))
1895
+ .join(", ")}).`,
1896
+ errorCode: "COLUMN_SELECTOR_NOT_FOUND",
1897
+ 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.",
1898
+ details: {
1899
+ select_columns: args.select_columns
1900
+ }
1901
+ });
1681
1902
  }
1682
1903
  const fitted = fitListItemsWithinSize({
1683
1904
  items: columnProjection.items,
@@ -1889,6 +2110,7 @@ async function executeRecordsSummary(args) {
1889
2110
  ...(args.time_range.to ? { max_value: args.time_range.to } : {})
1890
2111
  });
1891
2112
  }
2113
+ validateDateRangeFilters(summaryFilters, index, "qf_query(summary)");
1892
2114
  const listState = {
1893
2115
  query_id: queryId,
1894
2116
  app_key: args.app_key,
@@ -1904,6 +2126,7 @@ async function executeRecordsSummary(args) {
1904
2126
  : null
1905
2127
  };
1906
2128
  let currentPage = startPage;
2129
+ const startedAt = Date.now();
1907
2130
  let scannedPages = 0;
1908
2131
  let scannedRecords = 0;
1909
2132
  let hasMore = false;
@@ -1916,6 +2139,11 @@ async function executeRecordsSummary(args) {
1916
2139
  const rows = [];
1917
2140
  const byDay = new Map();
1918
2141
  while (scannedPages < requestedPages && scannedPages < scanMaxPages) {
2142
+ if (scannedPages > 0 && isExecutionBudgetExceeded(startedAt)) {
2143
+ hasMore = true;
2144
+ nextPageNum = currentPage;
2145
+ break;
2146
+ }
1919
2147
  const payload = buildListPayload({
1920
2148
  pageNum: currentPage,
1921
2149
  pageSize,
@@ -2123,6 +2351,7 @@ async function executeRecordsAggregate(args) {
2123
2351
  ...(args.time_range.to ? { max_value: args.time_range.to } : {})
2124
2352
  });
2125
2353
  }
2354
+ validateDateRangeFilters(aggregateFilters, index, "qf_records_aggregate");
2126
2355
  const listState = {
2127
2356
  query_id: queryId,
2128
2357
  app_key: args.app_key,
@@ -2138,6 +2367,7 @@ async function executeRecordsAggregate(args) {
2138
2367
  : null
2139
2368
  };
2140
2369
  let currentPage = startPage;
2370
+ const startedAt = Date.now();
2141
2371
  let scannedPages = 0;
2142
2372
  let scannedRecords = 0;
2143
2373
  let hasMore = false;
@@ -2148,6 +2378,11 @@ async function executeRecordsAggregate(args) {
2148
2378
  const sourcePages = [];
2149
2379
  const groupStats = new Map();
2150
2380
  while (scannedPages < requestedPages && scannedPages < scanMaxPages) {
2381
+ if (scannedPages > 0 && isExecutionBudgetExceeded(startedAt)) {
2382
+ hasMore = true;
2383
+ nextPageNum = currentPage;
2384
+ break;
2385
+ }
2151
2386
  const payload = buildListPayload({
2152
2387
  pageNum: currentPage,
2153
2388
  pageSize,
@@ -2981,14 +3216,17 @@ function toErrorPayload(error) {
2981
3216
  };
2982
3217
  }
2983
3218
  if (error instanceof QingflowApiError) {
3219
+ const timeoutHint = /timeout/i.test(error.message) || /timeout/i.test(error.errMsg);
2984
3220
  return {
2985
3221
  ok: false,
2986
- error_code: "QINGFLOW_API_ERROR",
3222
+ error_code: timeoutHint ? "UPSTREAM_TIMEOUT" : "QINGFLOW_API_ERROR",
2987
3223
  message: error.message,
2988
3224
  err_code: error.errCode,
2989
3225
  err_msg: error.errMsg || null,
2990
3226
  http_status: error.httpStatus,
2991
- fix_hint: "Check app_key/accessToken and request body against qf_form_get field definitions.",
3227
+ fix_hint: timeoutHint
3228
+ ? "Upstream request timed out. Reduce page_size/requested_pages, narrow filters, or continue with next_page_token."
3229
+ : "Check app_key/accessToken and request body against qf_form_get field definitions.",
2992
3230
  next_page_token: null,
2993
3231
  details: error.details ?? null
2994
3232
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qingflow-mcp",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
4
4
  "private": false,
5
5
  "license": "MIT",
6
6
  "type": "module",