qingflow-mcp 0.2.0 → 0.2.1

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 +14 -0
  2. package/dist/server.js +123 -9
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -93,6 +93,20 @@ MCP client config example:
93
93
  3. `qf_record_create` or `qf_record_update`.
94
94
  4. If create/update returns only `request_id`, call `qf_operation_get` to resolve async result.
95
95
 
96
+ ## List Query Tips
97
+
98
+ 1. For `qf_records_list.sort[].que_id`, use a real field `que_id` (numeric) or exact field title from `qf_form_get`.
99
+ 2. Avoid aliases like `create_time`; Qingflow often rejects them.
100
+ 3. When `include_answers=true`, the server auto-limits returned items to protect MCP context size.
101
+ 4. You can override item count with `max_items` in `qf_records_list`.
102
+
103
+ Optional env vars:
104
+
105
+ ```bash
106
+ export QINGFLOW_LIST_MAX_ITEMS_WITH_ANSWERS=5
107
+ export QINGFLOW_LIST_MAX_ITEMS_BYTES=400000
108
+ ```
109
+
96
110
  ## Publish
97
111
 
98
112
  ```bash
package/dist/server.js CHANGED
@@ -19,6 +19,9 @@ const MODE_TO_TYPE = {
19
19
  };
20
20
  const FORM_CACHE_TTL_MS = Number(process.env.QINGFLOW_FORM_CACHE_TTL_MS ?? "300000");
21
21
  const formCache = new Map();
22
+ const DEFAULT_PAGE_SIZE = 50;
23
+ const DEFAULT_MAX_ITEMS_WITH_ANSWERS = toPositiveInt(process.env.QINGFLOW_LIST_MAX_ITEMS_WITH_ANSWERS) ?? 5;
24
+ const MAX_LIST_ITEMS_BYTES = toPositiveInt(process.env.QINGFLOW_LIST_MAX_ITEMS_BYTES) ?? 400000;
22
25
  const accessToken = process.env.QINGFLOW_ACCESS_TOKEN;
23
26
  const baseUrl = process.env.QINGFLOW_BASE_URL;
24
27
  if (!accessToken) {
@@ -187,6 +190,7 @@ const listInputSchema = z.object({
187
190
  search_user_ids: z.array(z.string()).optional()
188
191
  }))
189
192
  .optional(),
193
+ max_items: z.number().int().positive().max(200).optional(),
190
194
  include_answers: z.boolean().optional()
191
195
  });
192
196
  const listOutputSchema = z.object({
@@ -358,36 +362,55 @@ server.registerTool("qf_records_list", {
358
362
  }
359
363
  }, async (args) => {
360
364
  try {
365
+ const pageNum = args.page_num ?? 1;
366
+ const pageSize = args.page_size ?? DEFAULT_PAGE_SIZE;
367
+ const normalizedSort = await normalizeListSort(args.sort, args.app_key, args.user_id);
361
368
  const payload = buildListPayload({
362
- pageNum: args.page_num ?? 1,
363
- pageSize: args.page_size ?? 50,
369
+ pageNum,
370
+ pageSize,
364
371
  mode: args.mode,
365
372
  type: args.type,
366
373
  keyword: args.keyword,
367
374
  queryLogic: args.query_logic,
368
375
  applyIds: args.apply_ids,
369
- sort: args.sort,
376
+ sort: normalizedSort,
370
377
  filters: args.filters
371
378
  });
372
379
  const response = await client.listRecords(args.app_key, payload, { userId: args.user_id });
373
380
  const result = asObject(response.result);
374
381
  const rawItems = asArray(result?.result);
375
382
  const includeAnswers = Boolean(args.include_answers);
376
- const items = rawItems.map((raw) => normalizeRecordItem(raw, includeAnswers));
383
+ const listLimit = resolveListItemLimit({
384
+ total: rawItems.length,
385
+ requestedMaxItems: args.max_items,
386
+ includeAnswers
387
+ });
388
+ const items = rawItems
389
+ .slice(0, listLimit.limit)
390
+ .map((raw) => normalizeRecordItem(raw, includeAnswers));
391
+ const fitted = fitListItemsWithinSize({
392
+ items,
393
+ limitBytes: MAX_LIST_ITEMS_BYTES
394
+ });
395
+ const truncationReason = mergeTruncationReasons(listLimit.reason, fitted.reason);
377
396
  return okResult({
378
397
  ok: true,
379
398
  data: {
380
399
  app_key: args.app_key,
381
400
  pagination: {
382
- page_num: toPositiveInt(result?.pageNum) ?? (args.page_num ?? 1),
383
- page_size: toPositiveInt(result?.pageSize) ?? (args.page_size ?? 50),
401
+ page_num: toPositiveInt(result?.pageNum) ?? pageNum,
402
+ page_size: toPositiveInt(result?.pageSize) ?? pageSize,
384
403
  page_amount: toNonNegativeInt(result?.pageAmount),
385
- result_amount: toNonNegativeInt(result?.resultAmount) ?? items.length
404
+ result_amount: toNonNegativeInt(result?.resultAmount) ?? fitted.items.length
386
405
  },
387
- items
406
+ items: fitted.items
388
407
  },
389
408
  meta: buildMeta(response)
390
- }, `Fetched ${items.length} records`);
409
+ }, buildRecordsListMessage({
410
+ returned: fitted.items.length,
411
+ total: rawItems.length,
412
+ truncationReason
413
+ }));
391
414
  }
392
415
  catch (error) {
393
416
  return errorResult(error);
@@ -789,6 +812,97 @@ function resolveFieldByKey(fieldKey, index) {
789
812
  }
790
813
  return null;
791
814
  }
815
+ async function normalizeListSort(sort, appKey, userId) {
816
+ if (!sort?.length) {
817
+ return sort;
818
+ }
819
+ // Fast path for numeric que_id, which can be passed through directly.
820
+ if (sort.every((item) => isNumericKey(String(item.que_id)))) {
821
+ return sort.map((item) => ({
822
+ que_id: Number(item.que_id),
823
+ ...(item.ascend !== undefined ? { ascend: item.ascend } : {})
824
+ }));
825
+ }
826
+ const form = await getFormCached(appKey, userId, false);
827
+ const index = buildFieldIndex(form.result);
828
+ return sort.map((item) => {
829
+ const rawKey = String(item.que_id).trim();
830
+ const resolved = resolveFieldByKey(rawKey, index);
831
+ if (!resolved || resolved.queId === undefined || resolved.queId === null) {
832
+ throw new Error(`Cannot resolve sort.que_id "${rawKey}". Use numeric que_id or exact field title from qf_form_get.`);
833
+ }
834
+ return {
835
+ que_id: normalizeQueId(resolved.queId),
836
+ ...(item.ascend !== undefined ? { ascend: item.ascend } : {})
837
+ };
838
+ });
839
+ }
840
+ function resolveListItemLimit(params) {
841
+ if (params.total <= 0) {
842
+ return { limit: 0, reason: null };
843
+ }
844
+ if (params.requestedMaxItems !== undefined) {
845
+ const limit = Math.min(params.total, params.requestedMaxItems);
846
+ if (limit < params.total) {
847
+ return {
848
+ limit,
849
+ reason: `limited by max_items=${params.requestedMaxItems}`
850
+ };
851
+ }
852
+ return { limit, reason: null };
853
+ }
854
+ if (params.includeAnswers && params.total > DEFAULT_MAX_ITEMS_WITH_ANSWERS) {
855
+ return {
856
+ limit: DEFAULT_MAX_ITEMS_WITH_ANSWERS,
857
+ reason: `auto-limited to ${DEFAULT_MAX_ITEMS_WITH_ANSWERS} items because include_answers=true`
858
+ };
859
+ }
860
+ return { limit: params.total, reason: null };
861
+ }
862
+ function fitListItemsWithinSize(params) {
863
+ let candidate = params.items;
864
+ let size = jsonSizeBytes(candidate);
865
+ if (size <= params.limitBytes) {
866
+ return { items: candidate, reason: null };
867
+ }
868
+ while (candidate.length > 1) {
869
+ candidate = candidate.slice(0, candidate.length - 1);
870
+ size = jsonSizeBytes(candidate);
871
+ if (size <= params.limitBytes) {
872
+ return {
873
+ items: candidate,
874
+ reason: `auto-limited to ${candidate.length} items to keep response <= ${params.limitBytes} bytes`
875
+ };
876
+ }
877
+ }
878
+ throw new Error(`qf_records_list response is too large (${size} bytes > ${params.limitBytes}) even with 1 item. Use qf_record_get(apply_id).`);
879
+ }
880
+ function mergeTruncationReasons(...reasons) {
881
+ const list = reasons.filter((item) => Boolean(item));
882
+ return list.length > 0 ? list.join("; ") : null;
883
+ }
884
+ function buildRecordsListMessage(params) {
885
+ if (!params.truncationReason) {
886
+ return `Fetched ${params.returned} records`;
887
+ }
888
+ return `Fetched ${params.returned}/${params.total} records (${params.truncationReason})`;
889
+ }
890
+ function normalizeQueId(queId) {
891
+ if (typeof queId === "number" && Number.isInteger(queId)) {
892
+ return queId;
893
+ }
894
+ if (typeof queId === "string") {
895
+ const normalized = queId.trim();
896
+ if (!normalized) {
897
+ throw new Error("Resolved que_id is empty");
898
+ }
899
+ return isNumericKey(normalized) ? Number(normalized) : normalized;
900
+ }
901
+ throw new Error(`Resolved que_id has unsupported type: ${typeof queId}`);
902
+ }
903
+ function jsonSizeBytes(value) {
904
+ return Buffer.byteLength(JSON.stringify(value), "utf8");
905
+ }
792
906
  function isNumericKey(value) {
793
907
  return /^\d+$/.test(value.trim());
794
908
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qingflow-mcp",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "private": false,
5
5
  "license": "MIT",
6
6
  "type": "module",