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.
Files changed (3) hide show
  1. package/README.md +4 -2
  2. package/dist/server.js +102 -43
  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.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 obj = asObject(raw);
1232
+ const parsedRoot = parseJsonLikeDeep(raw);
1233
+ const obj = asObject(parsedRoot);
1230
1234
  if (!obj) {
1231
- return raw;
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 obj = asObject(raw);
1257
+ const parsedRoot = parseJsonLikeDeep(raw);
1258
+ const obj = asObject(parsedRoot);
1254
1259
  if (!obj) {
1255
- return raw;
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 obj = asObject(raw);
1270
+ const parsedRoot = parseJsonLikeDeep(raw);
1271
+ const obj = asObject(parsedRoot);
1266
1272
  if (!obj) {
1267
- return raw;
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 obj = asObject(raw);
1296
+ const parsedRoot = parseJsonLikeDeep(raw);
1297
+ const obj = asObject(parsedRoot);
1291
1298
  if (!obj) {
1292
- return raw;
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
- if (typeof value === "string") {
1313
- const trimmed = value.trim();
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 value;
1332
+ return parsed;
1322
1333
  }
1323
1334
  function coerceBooleanLike(value) {
1324
- if (typeof value === "string") {
1325
- const trimmed = value.trim().toLowerCase();
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 value;
1348
+ return parsed;
1334
1349
  }
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;
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 = parseJsonLike(value);
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 = parseJsonLike(value);
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 = parseJsonLike(value);
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 = parseJsonLike(value);
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 = parseJsonLike(value);
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 Error(`No answers matched select_columns (${args.select_columns
1679
- .map((item) => String(item))
1680
- .join(", ")}). Check que_id/title from qf_form_get.`);
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: "Check app_key/accessToken and request body against qf_form_get field definitions.",
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
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qingflow-mcp",
3
- "version": "0.3.2",
3
+ "version": "0.3.3",
4
4
  "private": false,
5
5
  "license": "MIT",
6
6
  "type": "module",