qingflow-mcp 0.2.1 → 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.
package/README.md CHANGED
@@ -14,6 +14,10 @@ It intentionally excludes delete for now.
14
14
 
15
15
  ## Setup
16
16
 
17
+ Runtime requirement:
18
+
19
+ - Node.js `>=18`
20
+
17
21
  1. Install dependencies:
18
22
 
19
23
  ```bash
@@ -56,6 +60,12 @@ Global install from GitHub:
56
60
  npm i -g git+https://github.com/853046310/qingflow-mcp.git
57
61
  ```
58
62
 
63
+ Install latest from npm:
64
+
65
+ ```bash
66
+ npm i -g qingflow-mcp@latest
67
+ ```
68
+
59
69
  Or one-click installer:
60
70
 
61
71
  ```bash
@@ -97,8 +107,24 @@ MCP client config example:
97
107
 
98
108
  1. For `qf_records_list.sort[].que_id`, use a real field `que_id` (numeric) or exact field title from `qf_form_get`.
99
109
  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`.
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
+ ```
102
128
 
103
129
  Optional env vars:
104
130
 
@@ -107,6 +133,23 @@ export QINGFLOW_LIST_MAX_ITEMS_WITH_ANSWERS=5
107
133
  export QINGFLOW_LIST_MAX_ITEMS_BYTES=400000
108
134
  ```
109
135
 
136
+ ## Troubleshooting
137
+
138
+ If you see runtime errors around `Headers` or missing web APIs:
139
+
140
+ 1. Upgrade Node to `>=18`.
141
+ 2. Upgrade package to latest:
142
+
143
+ ```bash
144
+ npm i -g qingflow-mcp@latest
145
+ ```
146
+
147
+ 3. Verify runtime:
148
+
149
+ ```bash
150
+ node -e "console.log(process.version, typeof fetch, typeof Headers)"
151
+ ```
152
+
110
153
  ## Publish
111
154
 
112
155
  ```bash
@@ -88,24 +88,25 @@ export class QingflowClient {
88
88
  const options = params.options ?? {};
89
89
  const url = new URL(params.path, this.baseUrl);
90
90
  appendQuery(url, options.query);
91
- const headers = new Headers();
92
- headers.set("accessToken", this.accessToken);
91
+ const headers = {
92
+ accessToken: this.accessToken
93
+ };
93
94
  if (options.userId) {
94
- headers.set("userId", options.userId);
95
+ headers.userId = options.userId;
95
96
  }
96
97
  const init = {
97
98
  method: params.method,
98
99
  headers
99
100
  };
100
101
  if (options.body !== undefined) {
101
- headers.set("content-type", "application/json");
102
+ headers["content-type"] = "application/json";
102
103
  init.body = JSON.stringify(options.body);
103
104
  }
104
105
  const controller = new AbortController();
105
106
  const timer = setTimeout(() => controller.abort(), this.timeoutMs);
106
107
  init.signal = controller.signal;
107
108
  try {
108
- const response = await fetch(url, init);
109
+ const response = await getFetch()(url, init);
109
110
  const text = await response.text();
110
111
  const data = safeJsonParse(text);
111
112
  if (!response.ok) {
@@ -253,3 +254,12 @@ function toFiniteNumber(value) {
253
254
  }
254
255
  return null;
255
256
  }
257
+ function getFetch() {
258
+ const runtimeFetch = globalThis.fetch;
259
+ if (typeof runtimeFetch === "function") {
260
+ return runtimeFetch.bind(globalThis);
261
+ }
262
+ throw new QingflowApiError({
263
+ message: "Global fetch is not available. Use Node.js >= 18."
264
+ });
265
+ }
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.1",
3
+ "version": "0.2.3",
4
4
  "private": false,
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -35,6 +35,9 @@
35
35
  "start": "node dist/server.js",
36
36
  "prepublishOnly": "npm run build"
37
37
  },
38
+ "engines": {
39
+ "node": ">=18"
40
+ },
38
41
  "dependencies": {
39
42
  "@modelcontextprotocol/sdk": "^1.17.4",
40
43
  "zod": "^3.25.76"