qingflow-mcp 0.2.2 → 0.2.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 +28 -2
  2. package/dist/server.js +161 -11
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -107,8 +107,34 @@ 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
+ ```
128
+
129
+ For single record details (`qf_record_get`), the same column controls are supported:
130
+
131
+ ```json
132
+ {
133
+ "apply_id": "497600278750478338",
134
+ "max_columns": 5,
135
+ "select_columns": [1, "客户名称"]
136
+ }
137
+ ```
112
138
 
113
139
  Optional env vars:
114
140
 
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,19 +210,39 @@ 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
  });
210
225
  const recordGetInputSchema = z.object({
211
- apply_id: z.union([z.string().min(1), z.number().int()])
226
+ apply_id: z.union([z.string().min(1), z.number().int()]),
227
+ max_columns: z.number().int().positive().max(200).optional(),
228
+ select_columns: z
229
+ .array(z.union([z.string().min(1), z.number().int()]))
230
+ .min(1)
231
+ .max(200)
232
+ .optional()
212
233
  });
213
234
  const recordGetOutputSchema = z.object({
214
235
  ok: z.literal(true),
215
236
  data: z.object({
216
237
  apply_id: z.union([z.string(), z.number(), z.null()]),
217
238
  answer_count: z.number().int().nonnegative(),
218
- record: z.unknown()
239
+ record: z.unknown(),
240
+ applied_limits: z
241
+ .object({
242
+ column_cap: z.number().int().positive().nullable(),
243
+ selected_columns: z.array(z.string()).nullable()
244
+ })
245
+ .optional()
219
246
  }),
220
247
  meta: apiMetaSchema
221
248
  });
@@ -365,6 +392,7 @@ server.registerTool("qf_records_list", {
365
392
  const pageNum = args.page_num ?? 1;
366
393
  const pageSize = args.page_size ?? DEFAULT_PAGE_SIZE;
367
394
  const normalizedSort = await normalizeListSort(args.sort, args.app_key, args.user_id);
395
+ const includeAnswers = Boolean(args.include_answers || (args.select_columns && args.select_columns.length > 0));
368
396
  const payload = buildListPayload({
369
397
  pageNum,
370
398
  pageSize,
@@ -379,20 +407,26 @@ server.registerTool("qf_records_list", {
379
407
  const response = await client.listRecords(args.app_key, payload, { userId: args.user_id });
380
408
  const result = asObject(response.result);
381
409
  const rawItems = asArray(result?.result);
382
- const includeAnswers = Boolean(args.include_answers);
383
410
  const listLimit = resolveListItemLimit({
384
411
  total: rawItems.length,
412
+ requestedMaxRows: args.max_rows,
385
413
  requestedMaxItems: args.max_items,
386
414
  includeAnswers
387
415
  });
388
416
  const items = rawItems
389
417
  .slice(0, listLimit.limit)
390
418
  .map((raw) => normalizeRecordItem(raw, includeAnswers));
391
- const fitted = fitListItemsWithinSize({
419
+ const columnProjection = projectRecordItemsColumns({
392
420
  items,
421
+ includeAnswers,
422
+ maxColumns: args.max_columns,
423
+ selectColumns: args.select_columns
424
+ });
425
+ const fitted = fitListItemsWithinSize({
426
+ items: columnProjection.items,
393
427
  limitBytes: MAX_LIST_ITEMS_BYTES
394
428
  });
395
- const truncationReason = mergeTruncationReasons(listLimit.reason, fitted.reason);
429
+ const truncationReason = mergeTruncationReasons(listLimit.reason, columnProjection.reason, fitted.reason);
396
430
  return okResult({
397
431
  ok: true,
398
432
  data: {
@@ -403,7 +437,13 @@ server.registerTool("qf_records_list", {
403
437
  page_amount: toNonNegativeInt(result?.pageAmount),
404
438
  result_amount: toNonNegativeInt(result?.resultAmount) ?? fitted.items.length
405
439
  },
406
- items: fitted.items
440
+ items: fitted.items,
441
+ applied_limits: {
442
+ include_answers: includeAnswers,
443
+ row_cap: listLimit.limit,
444
+ column_cap: args.max_columns ?? null,
445
+ selected_columns: columnProjection.selectedColumns
446
+ }
407
447
  },
408
448
  meta: buildMeta(response)
409
449
  }, buildRecordsListMessage({
@@ -429,13 +469,26 @@ server.registerTool("qf_record_get", {
429
469
  try {
430
470
  const response = await client.getRecord(String(args.apply_id));
431
471
  const record = asObject(response.result) ?? {};
432
- const answerCount = asArray(record.answers).length;
472
+ const projection = projectAnswersForOutput({
473
+ answers: asArray(record.answers),
474
+ maxColumns: args.max_columns,
475
+ selectColumns: args.select_columns
476
+ });
477
+ const projectedRecord = {
478
+ ...record,
479
+ answers: projection.answers
480
+ };
481
+ const answerCount = projection.answers.length;
433
482
  return okResult({
434
483
  ok: true,
435
484
  data: {
436
485
  apply_id: record.applyId ?? null,
437
486
  answer_count: answerCount,
438
- record: response.result
487
+ record: projectedRecord,
488
+ applied_limits: {
489
+ column_cap: args.max_columns ?? null,
490
+ selected_columns: projection.selectedColumns
491
+ }
439
492
  },
440
493
  meta: buildMeta(response)
441
494
  }, `Fetched record ${String(args.apply_id)}`);
@@ -841,12 +894,21 @@ function resolveListItemLimit(params) {
841
894
  if (params.total <= 0) {
842
895
  return { limit: 0, reason: null };
843
896
  }
897
+ const explicitLimits = [];
898
+ if (params.requestedMaxRows !== undefined) {
899
+ explicitLimits.push({ name: "max_rows", value: params.requestedMaxRows });
900
+ }
844
901
  if (params.requestedMaxItems !== undefined) {
845
- const limit = Math.min(params.total, params.requestedMaxItems);
902
+ explicitLimits.push({ name: "max_items", value: params.requestedMaxItems });
903
+ }
904
+ if (explicitLimits.length > 0) {
905
+ const limit = Math.min(params.total, ...explicitLimits.map((item) => item.value));
846
906
  if (limit < params.total) {
847
907
  return {
848
908
  limit,
849
- reason: `limited by max_items=${params.requestedMaxItems}`
909
+ reason: `limited by ${explicitLimits
910
+ .map((item) => `${item.name}=${item.value}`)
911
+ .join(", ")} (effective=${limit})`
850
912
  };
851
913
  }
852
914
  return { limit, reason: null };
@@ -859,6 +921,56 @@ function resolveListItemLimit(params) {
859
921
  }
860
922
  return { limit: params.total, reason: null };
861
923
  }
924
+ function projectRecordItemsColumns(params) {
925
+ if (!params.includeAnswers) {
926
+ return {
927
+ items: params.items,
928
+ reason: null,
929
+ selectedColumns: null
930
+ };
931
+ }
932
+ const normalizedSelectors = normalizeColumnSelectors(params.selectColumns);
933
+ const selectorSet = new Set(normalizedSelectors.map((item) => normalizeColumnSelector(item)));
934
+ let columnCapped = false;
935
+ const projectedItems = params.items.map((item) => {
936
+ const answers = asArray(item.answers);
937
+ let projected = answers;
938
+ if (selectorSet.size > 0) {
939
+ projected = answers.filter((answer) => answerMatchesAnySelector(answer, selectorSet));
940
+ }
941
+ if (params.maxColumns !== undefined && projected.length > params.maxColumns) {
942
+ projected = projected.slice(0, params.maxColumns);
943
+ columnCapped = true;
944
+ }
945
+ return {
946
+ ...item,
947
+ answers: projected
948
+ };
949
+ });
950
+ const reason = mergeTruncationReasons(selectorSet.size > 0 ? `selected columns=${normalizedSelectors.length}` : null, columnCapped && params.maxColumns !== undefined
951
+ ? `limited to max_columns=${params.maxColumns}`
952
+ : null);
953
+ return {
954
+ items: projectedItems,
955
+ reason,
956
+ selectedColumns: normalizedSelectors.length > 0 ? normalizedSelectors : null
957
+ };
958
+ }
959
+ function projectAnswersForOutput(params) {
960
+ const normalizedSelectors = normalizeColumnSelectors(params.selectColumns);
961
+ const selectorSet = new Set(normalizedSelectors.map((item) => normalizeColumnSelector(item)));
962
+ let projected = params.answers;
963
+ if (selectorSet.size > 0) {
964
+ projected = projected.filter((answer) => answerMatchesAnySelector(answer, selectorSet));
965
+ }
966
+ if (params.maxColumns !== undefined && projected.length > params.maxColumns) {
967
+ projected = projected.slice(0, params.maxColumns);
968
+ }
969
+ return {
970
+ answers: projected,
971
+ selectedColumns: normalizedSelectors.length > 0 ? normalizedSelectors : null
972
+ };
973
+ }
862
974
  function fitListItemsWithinSize(params) {
863
975
  let candidate = params.items;
864
976
  let size = jsonSizeBytes(candidate);
@@ -887,6 +999,44 @@ function buildRecordsListMessage(params) {
887
999
  }
888
1000
  return `Fetched ${params.returned}/${params.total} records (${params.truncationReason})`;
889
1001
  }
1002
+ function normalizeColumnSelectors(selectColumns) {
1003
+ if (!selectColumns?.length) {
1004
+ return [];
1005
+ }
1006
+ const deduped = new Set();
1007
+ for (const value of selectColumns) {
1008
+ const normalized = String(value).trim();
1009
+ if (!normalized) {
1010
+ continue;
1011
+ }
1012
+ deduped.add(normalized);
1013
+ }
1014
+ return Array.from(deduped);
1015
+ }
1016
+ function normalizeColumnSelector(value) {
1017
+ if (typeof value === "number" && Number.isFinite(value)) {
1018
+ return `id:${Math.trunc(value)}`;
1019
+ }
1020
+ const normalized = String(value).trim();
1021
+ if (!normalized) {
1022
+ return "title:";
1023
+ }
1024
+ if (isNumericKey(normalized)) {
1025
+ return `id:${Number(normalized)}`;
1026
+ }
1027
+ return `title:${normalized.toLowerCase()}`;
1028
+ }
1029
+ function answerMatchesAnySelector(answer, selectorSet) {
1030
+ const obj = asObject(answer);
1031
+ if (!obj) {
1032
+ return false;
1033
+ }
1034
+ const candidates = [
1035
+ normalizeColumnSelector(asNullableString(obj.queId) ?? ""),
1036
+ normalizeColumnSelector(asNullableString(obj.queTitle) ?? "")
1037
+ ];
1038
+ return candidates.some((candidate) => selectorSet.has(candidate));
1039
+ }
890
1040
  function normalizeQueId(queId) {
891
1041
  if (typeof queId === "number" && Number.isInteger(queId)) {
892
1042
  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.4",
4
4
  "private": false,
5
5
  "license": "MIT",
6
6
  "type": "module",