qingflow-mcp 0.2.2 → 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.
Files changed (3) hide show
  1. package/README.md +18 -2
  2. package/dist/server.js +117 -7
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -107,8 +107,24 @@ MCP client config example:
107
107
 
108
108
  1. For `qf_records_list.sort[].que_id`, use a real field `que_id` (numeric) or exact field title from `qf_form_get`.
109
109
  2. Avoid aliases like `create_time`; Qingflow often rejects them.
110
- 3. When `include_answers=true`, the server auto-limits returned items to protect MCP context size.
111
- 4. You can override item count with `max_items` in `qf_records_list`.
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
+ ```
112
128
 
113
129
  Optional env vars:
114
130
 
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 fitted = fitListItemsWithinSize({
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
- const limit = Math.min(params.total, params.requestedMaxItems);
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 max_items=${params.requestedMaxItems}`
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.2",
3
+ "version": "0.2.3",
4
4
  "private": false,
5
5
  "license": "MIT",
6
6
  "type": "module",