qingflow-mcp 0.3.0 → 0.3.2

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 -0
  2. package/dist/server.js +658 -23
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -60,6 +60,31 @@ Run tests:
60
60
  npm test
61
61
  ```
62
62
 
63
+ ## Command Line Usage
64
+
65
+ `qingflow-mcp` still defaults to MCP stdio mode:
66
+
67
+ ```bash
68
+ qingflow-mcp
69
+ ```
70
+
71
+ Use CLI mode for quick local invocation:
72
+
73
+ ```bash
74
+ # list all available tools
75
+ qingflow-mcp cli tools
76
+
77
+ # machine-readable tool list
78
+ qingflow-mcp cli tools --json
79
+
80
+ # call one tool with JSON args
81
+ qingflow-mcp cli call qf_apps_list --args '{"limit":5}'
82
+
83
+ # call from stdin
84
+ echo '{"app_key":"your_app_key","mode":"all","select_columns":[1001]}' \
85
+ | qingflow-mcp cli call qf_query
86
+ ```
87
+
63
88
  ## CLI Install
64
89
 
65
90
  Global install from GitHub:
@@ -164,6 +189,9 @@ Deterministic read protocol (list/summary/aggregate):
164
189
  - `time_range`
165
190
  - `source_pages`
166
191
  3. `strict_full=true` makes incomplete results fail fast with `NEED_MORE_DATA`.
192
+ 4. Success payloads also expose top-level `error_code=null`, `fix_hint=null`, `next_page_token`.
193
+ 5. Error payloads expose `error_code` and `fix_hint` for actionable retries.
194
+ 6. Parameter tolerance supports stringified JSON and numeric/boolean strings for key query fields.
167
195
 
168
196
  ## List Query Tips
169
197
 
package/dist/server.js CHANGED
@@ -1,6 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
5
+ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
4
6
  import { randomUUID } from "node:crypto";
5
7
  import { z } from "zod";
6
8
  import { QingflowApiError, QingflowClient } from "./qingflow-client.js";
@@ -27,6 +29,18 @@ class NeedMoreDataError extends Error {
27
29
  this.details = details;
28
30
  }
29
31
  }
32
+ class InputValidationError extends Error {
33
+ errorCode;
34
+ fixHint;
35
+ details;
36
+ constructor(params) {
37
+ super(params.message);
38
+ this.name = "InputValidationError";
39
+ this.errorCode = params.errorCode;
40
+ this.fixHint = params.fixHint;
41
+ this.details = params.details ?? null;
42
+ }
43
+ }
30
44
  const FORM_CACHE_TTL_MS = Number(process.env.QINGFLOW_FORM_CACHE_TTL_MS ?? "300000");
31
45
  const formCache = new Map();
32
46
  const DEFAULT_PAGE_SIZE = 50;
@@ -47,7 +61,7 @@ const client = new QingflowClient({
47
61
  });
48
62
  const server = new McpServer({
49
63
  name: "qingflow-mcp",
50
- version: "0.3.0"
64
+ version: "0.3.1"
51
65
  });
52
66
  const jsonPrimitiveSchema = z.union([z.string(), z.number(), z.boolean(), z.null()]);
53
67
  const answerValueSchema = z.union([
@@ -129,6 +143,13 @@ const evidenceSchema = z.object({
129
143
  .nullable(),
130
144
  source_pages: z.array(z.number().int().positive())
131
145
  });
146
+ const queryContractFields = {
147
+ completeness: completenessSchema,
148
+ evidence: z.record(z.unknown()),
149
+ error_code: z.null(),
150
+ fix_hint: z.null(),
151
+ next_page_token: z.string().nullable()
152
+ };
132
153
  const appSchema = z.object({
133
154
  appKey: z.string(),
134
155
  appName: z.string()
@@ -189,8 +210,8 @@ const formSuccessOutputSchema = z.object({
189
210
  });
190
211
  const formOutputSchema = formSuccessOutputSchema;
191
212
  const listInputSchema = z
192
- .object({
193
- app_key: z.string().min(1),
213
+ .preprocess(normalizeListInput, z.object({
214
+ app_key: z.string().min(1).optional(),
194
215
  user_id: z.string().min(1).optional(),
195
216
  page_num: z.number().int().positive().optional(),
196
217
  page_token: z.string().min(1).optional(),
@@ -247,10 +268,14 @@ const listInputSchema = z
247
268
  max_items: z.number().int().positive().max(200).optional(),
248
269
  max_columns: z.number().int().positive().max(10).optional(),
249
270
  // Strict mode: callers must explicitly choose columns.
250
- select_columns: z.array(z.union([z.string().min(1), z.number().int()])).min(1).max(10),
271
+ select_columns: z
272
+ .array(z.union([z.string().min(1), z.number().int()]))
273
+ .min(1)
274
+ .max(10)
275
+ .optional(),
251
276
  include_answers: z.boolean().optional(),
252
277
  strict_full: z.boolean().optional()
253
- })
278
+ }))
254
279
  .refine((value) => value.include_answers !== false, {
255
280
  message: "include_answers=false is not allowed in strict column mode"
256
281
  })
@@ -279,17 +304,19 @@ const listSuccessOutputSchema = z.object({
279
304
  completeness: completenessSchema,
280
305
  evidence: evidenceSchema
281
306
  }),
307
+ ...queryContractFields,
282
308
  meta: apiMetaSchema
283
309
  });
284
310
  const listOutputSchema = listSuccessOutputSchema;
285
- const recordGetInputSchema = z.object({
311
+ const recordGetInputSchema = z.preprocess(normalizeRecordGetInput, z.object({
286
312
  apply_id: z.union([z.string().min(1), z.number().int()]),
287
313
  max_columns: z.number().int().positive().max(10).optional(),
288
314
  select_columns: z
289
315
  .array(z.union([z.string().min(1), z.number().int()]))
290
316
  .min(1)
291
317
  .max(10)
292
- });
318
+ .optional()
319
+ }));
293
320
  const recordGetSuccessOutputSchema = z.object({
294
321
  ok: z.literal(true),
295
322
  data: z.object({
@@ -309,6 +336,7 @@ const recordGetSuccessOutputSchema = z.object({
309
336
  selected_columns: z.array(z.string())
310
337
  })
311
338
  }),
339
+ ...queryContractFields,
312
340
  meta: apiMetaSchema
313
341
  });
314
342
  const recordGetOutputSchema = recordGetSuccessOutputSchema;
@@ -371,7 +399,8 @@ const operationSuccessOutputSchema = z.object({
371
399
  meta: apiMetaSchema
372
400
  });
373
401
  const operationOutputSchema = operationSuccessOutputSchema;
374
- const queryInputSchema = z.object({
402
+ const queryInputSchema = z
403
+ .preprocess(normalizeQueryInput, z.object({
375
404
  query_mode: z.enum(["auto", "list", "record", "summary"]).optional(),
376
405
  app_key: z.string().min(1).optional(),
377
406
  apply_id: z.union([z.string().min(1), z.number().int()]).optional(),
@@ -444,7 +473,7 @@ const queryInputSchema = z.object({
444
473
  .optional(),
445
474
  scan_max_pages: z.number().int().positive().max(500).optional(),
446
475
  strict_full: z.boolean().optional()
447
- })
476
+ }))
448
477
  .refine((value) => !(value.page_num !== undefined && value.page_token !== undefined), {
449
478
  message: "page_num and page_token cannot be used together"
450
479
  });
@@ -504,11 +533,12 @@ const querySuccessOutputSchema = z.object({
504
533
  record: recordGetSuccessOutputSchema.shape.data.optional(),
505
534
  summary: querySummaryOutputSchema.optional()
506
535
  }),
536
+ ...queryContractFields,
507
537
  meta: apiMetaSchema
508
538
  });
509
539
  const queryOutputSchema = querySuccessOutputSchema;
510
540
  const aggregateInputSchema = z
511
- .object({
541
+ .preprocess(normalizeAggregateInput, z.object({
512
542
  app_key: z.string().min(1),
513
543
  user_id: z.string().min(1).optional(),
514
544
  page_num: z.number().int().positive().optional(),
@@ -572,7 +602,7 @@ const aggregateInputSchema = z
572
602
  .optional(),
573
603
  max_groups: z.number().int().positive().max(2000).optional(),
574
604
  strict_full: z.boolean().optional()
575
- })
605
+ }))
576
606
  .refine((value) => !(value.page_num !== undefined && value.page_token !== undefined), {
577
607
  message: "page_num and page_token cannot be used together"
578
608
  });
@@ -607,6 +637,7 @@ const aggregateOutputSchema = z.object({
607
637
  })
608
638
  })
609
639
  }),
640
+ ...queryContractFields,
610
641
  meta: apiMetaSchema
611
642
  });
612
643
  server.registerTool("qf_apps_list", {
@@ -736,6 +767,8 @@ server.registerTool("qf_query", {
736
767
  if (routedMode === "record") {
737
768
  const recordArgs = buildRecordGetArgsFromQuery(args);
738
769
  const executed = await executeRecordGet(recordArgs);
770
+ const completeness = executed.payload.completeness;
771
+ const evidence = executed.payload.evidence;
739
772
  return okResult({
740
773
  ok: true,
741
774
  data: {
@@ -743,11 +776,18 @@ server.registerTool("qf_query", {
743
776
  source_tool: "qf_record_get",
744
777
  record: executed.payload.data
745
778
  },
779
+ completeness,
780
+ evidence,
781
+ error_code: null,
782
+ fix_hint: null,
783
+ next_page_token: completeness.next_page_token,
746
784
  meta: executed.payload.meta
747
785
  }, executed.message);
748
786
  }
749
787
  if (routedMode === "summary") {
750
788
  const executed = await executeRecordsSummary(args);
789
+ const completeness = executed.data.completeness;
790
+ const evidence = executed.data.evidence;
751
791
  return okResult({
752
792
  ok: true,
753
793
  data: {
@@ -755,11 +795,18 @@ server.registerTool("qf_query", {
755
795
  source_tool: "qf_records_summary",
756
796
  summary: executed.data
757
797
  },
798
+ completeness,
799
+ evidence,
800
+ error_code: null,
801
+ fix_hint: null,
802
+ next_page_token: completeness.next_page_token,
758
803
  meta: executed.meta
759
804
  }, executed.message);
760
805
  }
761
806
  const listArgs = buildListArgsFromQuery(args);
762
807
  const executed = await executeRecordsList(listArgs);
808
+ const completeness = executed.payload.completeness;
809
+ const evidence = executed.payload.evidence;
763
810
  return okResult({
764
811
  ok: true,
765
812
  data: {
@@ -767,6 +814,11 @@ server.registerTool("qf_query", {
767
814
  source_tool: "qf_records_list",
768
815
  list: executed.payload.data
769
816
  },
817
+ completeness,
818
+ evidence,
819
+ error_code: null,
820
+ fix_hint: null,
821
+ next_page_token: completeness.next_page_token,
770
822
  meta: executed.payload.meta
771
823
  }, executed.message);
772
824
  }
@@ -899,10 +951,259 @@ server.registerTool("qf_records_aggregate", {
899
951
  }
900
952
  });
901
953
  async function main() {
954
+ const cliExitCode = await runCli(process.argv.slice(2));
955
+ if (cliExitCode !== null) {
956
+ process.exitCode = cliExitCode;
957
+ return;
958
+ }
902
959
  const transport = new StdioServerTransport();
903
960
  await server.connect(transport);
904
961
  }
905
- void main();
962
+ void main().catch((error) => {
963
+ process.stderr.write(`${error instanceof Error ? error.message : "Unknown error"}\n`);
964
+ process.exitCode = 1;
965
+ });
966
+ async function runCli(argv) {
967
+ if (argv.length === 0 || argv[0] === "--stdio-server") {
968
+ return null;
969
+ }
970
+ const [command, ...rest] = argv;
971
+ if (command === "--help" || command === "-h") {
972
+ printCliHelp();
973
+ return 0;
974
+ }
975
+ if (command !== "cli") {
976
+ printCliHelp();
977
+ return 2;
978
+ }
979
+ const [subcommand, ...subArgs] = rest;
980
+ if (!subcommand || subcommand === "--help" || subcommand === "-h") {
981
+ printCliHelp();
982
+ return 0;
983
+ }
984
+ if (subcommand === "tools") {
985
+ return runCliTools(subArgs);
986
+ }
987
+ if (subcommand === "call") {
988
+ return runCliCall(subArgs);
989
+ }
990
+ process.stderr.write(`Unknown CLI subcommand: ${subcommand}\n`);
991
+ printCliHelp();
992
+ return 2;
993
+ }
994
+ async function runCliTools(args) {
995
+ let options;
996
+ try {
997
+ options = parseCliFlags(args);
998
+ }
999
+ catch (error) {
1000
+ process.stderr.write(`${error instanceof Error ? error.message : "Invalid CLI options"}\n`);
1001
+ return 2;
1002
+ }
1003
+ if (options.help) {
1004
+ process.stdout.write("Usage: qingflow-mcp cli tools [--json]\n");
1005
+ return 0;
1006
+ }
1007
+ if (options.argsText !== undefined) {
1008
+ process.stderr.write("--args is not supported for 'cli tools'\n");
1009
+ return 2;
1010
+ }
1011
+ const call = await callLocalMcp("tools");
1012
+ if (call.ok) {
1013
+ if (options.json) {
1014
+ process.stdout.write(`${JSON.stringify(call.tools, null, 2)}\n`);
1015
+ return 0;
1016
+ }
1017
+ for (const tool of call.tools) {
1018
+ process.stdout.write(`${tool.name}\t${tool.description ?? ""}\n`);
1019
+ }
1020
+ return 0;
1021
+ }
1022
+ process.stderr.write(`${JSON.stringify(call.error, null, 2)}\n`);
1023
+ return 1;
1024
+ }
1025
+ async function runCliCall(args) {
1026
+ if (args.length === 0) {
1027
+ process.stderr.write("Usage: qingflow-mcp cli call <tool_name> [--args '{\"key\":\"value\"}']\n");
1028
+ return 2;
1029
+ }
1030
+ const [toolName, ...flagArgs] = args;
1031
+ let options;
1032
+ try {
1033
+ options = parseCliFlags(flagArgs);
1034
+ }
1035
+ catch (error) {
1036
+ process.stderr.write(`${error instanceof Error ? error.message : "Invalid CLI options"}\n`);
1037
+ return 2;
1038
+ }
1039
+ if (options.help) {
1040
+ process.stdout.write("Usage: qingflow-mcp cli call <tool_name> [--args '{\"key\":\"value\"}'] [--json]\n");
1041
+ return 0;
1042
+ }
1043
+ const inputText = options.argsText ?? (process.stdin.isTTY ? "{}" : await readStdinText());
1044
+ let parsedInput;
1045
+ try {
1046
+ parsedInput = inputText.trim() ? JSON.parse(inputText) : {};
1047
+ }
1048
+ catch {
1049
+ process.stderr.write("Invalid JSON for --args or stdin body\n");
1050
+ return 2;
1051
+ }
1052
+ if (!parsedInput || typeof parsedInput !== "object" || Array.isArray(parsedInput)) {
1053
+ process.stderr.write("Tool arguments must be a JSON object\n");
1054
+ return 2;
1055
+ }
1056
+ const call = await callLocalMcp("call", {
1057
+ toolName,
1058
+ args: parsedInput
1059
+ });
1060
+ if (call.ok) {
1061
+ if (options.json || typeof call.payload === "object") {
1062
+ process.stdout.write(`${JSON.stringify(call.payload, null, 2)}\n`);
1063
+ return 0;
1064
+ }
1065
+ process.stdout.write(`${String(call.payload)}\n`);
1066
+ return 0;
1067
+ }
1068
+ process.stderr.write(`${JSON.stringify(call.error, null, 2)}\n`);
1069
+ return 1;
1070
+ }
1071
+ function parseCliFlags(args) {
1072
+ let argsText;
1073
+ let help = false;
1074
+ let json = false;
1075
+ for (let i = 0; i < args.length; i += 1) {
1076
+ const token = args[i];
1077
+ if (token === "--help" || token === "-h") {
1078
+ help = true;
1079
+ continue;
1080
+ }
1081
+ if (token === "--json") {
1082
+ json = true;
1083
+ continue;
1084
+ }
1085
+ if (token === "--args") {
1086
+ const next = args[i + 1];
1087
+ if (next === undefined) {
1088
+ throw new Error("--args requires a JSON value");
1089
+ }
1090
+ argsText = next;
1091
+ i += 1;
1092
+ continue;
1093
+ }
1094
+ if (token.startsWith("--args=")) {
1095
+ argsText = token.slice("--args=".length);
1096
+ continue;
1097
+ }
1098
+ throw new Error(`Unknown CLI option: ${token}`);
1099
+ }
1100
+ return { argsText, help, json };
1101
+ }
1102
+ async function callLocalMcp(mode, params) {
1103
+ const entrypoint = process.argv[1];
1104
+ if (!entrypoint) {
1105
+ return { ok: false, error: { message: "Cannot locate current executable entrypoint" } };
1106
+ }
1107
+ const childEnv = {};
1108
+ for (const [key, value] of Object.entries(process.env)) {
1109
+ if (value !== undefined) {
1110
+ childEnv[key] = value;
1111
+ }
1112
+ }
1113
+ const transport = new StdioClientTransport({
1114
+ command: process.execPath,
1115
+ args: [entrypoint, "--stdio-server"],
1116
+ cwd: process.cwd(),
1117
+ env: childEnv,
1118
+ stderr: "pipe"
1119
+ });
1120
+ if (transport.stderr) {
1121
+ transport.stderr.on("data", () => {
1122
+ // Keep stderr drained to avoid child process backpressure.
1123
+ });
1124
+ }
1125
+ const localClient = new Client({
1126
+ name: "qingflow-mcp-cli",
1127
+ version: "0.3.0"
1128
+ });
1129
+ try {
1130
+ await localClient.connect(transport);
1131
+ if (mode === "tools") {
1132
+ const listed = await localClient.listTools();
1133
+ const tools = listed.tools.map((tool) => ({
1134
+ name: tool.name,
1135
+ description: tool.description
1136
+ }));
1137
+ return { ok: true, tools };
1138
+ }
1139
+ if (!params) {
1140
+ throw new Error("Missing tool call params");
1141
+ }
1142
+ const result = await localClient.callTool({
1143
+ name: params.toolName,
1144
+ arguments: params.args
1145
+ });
1146
+ if (result.isError) {
1147
+ const payload = tryParseToolPayload(result);
1148
+ return { ok: false, error: payload ?? { message: `Tool ${params.toolName} failed`, result } };
1149
+ }
1150
+ const payload = tryParseToolPayload(result);
1151
+ return { ok: true, payload: payload ?? result };
1152
+ }
1153
+ catch (error) {
1154
+ return {
1155
+ ok: false,
1156
+ error: error instanceof Error ? { message: error.message } : error
1157
+ };
1158
+ }
1159
+ finally {
1160
+ await localClient.close().catch(() => { });
1161
+ }
1162
+ }
1163
+ function tryParseToolPayload(result) {
1164
+ const obj = asObject(result);
1165
+ if (!obj) {
1166
+ return null;
1167
+ }
1168
+ if (obj.structuredContent !== undefined) {
1169
+ return obj.structuredContent;
1170
+ }
1171
+ const textItems = Array.isArray(obj.content)
1172
+ ? obj.content.filter((item) => Boolean(item) &&
1173
+ typeof item === "object" &&
1174
+ item.type === "text" &&
1175
+ typeof item.text === "string")
1176
+ : [];
1177
+ for (const item of textItems) {
1178
+ const text = item.text;
1179
+ try {
1180
+ return JSON.parse(text);
1181
+ }
1182
+ catch {
1183
+ // keep scanning and fallback
1184
+ }
1185
+ }
1186
+ return null;
1187
+ }
1188
+ async function readStdinText() {
1189
+ const chunks = [];
1190
+ for await (const chunk of process.stdin) {
1191
+ chunks.push(String(chunk));
1192
+ }
1193
+ return chunks.join("");
1194
+ }
1195
+ function printCliHelp() {
1196
+ process.stdout.write(`qingflow-mcp usage
1197
+
1198
+ Default (MCP stdio server):
1199
+ qingflow-mcp
1200
+
1201
+ CLI mode:
1202
+ qingflow-mcp cli tools [--json]
1203
+ qingflow-mcp cli call <tool_name> [--args '{"key":"value"}'] [--json]
1204
+ echo '{"app_key":"xxx","mode":"all","select_columns":[1001]}' | qingflow-mcp cli call qf_query
1205
+ `);
1206
+ }
906
1207
  function hasWritePayload(answers, fields) {
907
1208
  return Boolean((answers && answers.length > 0) || (fields && Object.keys(fields).length > 0));
908
1209
  }
@@ -913,6 +1214,227 @@ function buildMeta(response) {
913
1214
  base_url: baseUrl
914
1215
  };
915
1216
  }
1217
+ function missingRequiredFieldError(params) {
1218
+ return new InputValidationError({
1219
+ message: `Missing required field "${params.field}" for ${params.tool}`,
1220
+ errorCode: "MISSING_REQUIRED_FIELD",
1221
+ fixHint: params.fixHint,
1222
+ details: {
1223
+ field: params.field,
1224
+ tool: params.tool
1225
+ }
1226
+ });
1227
+ }
1228
+ function normalizeListInput(raw) {
1229
+ const obj = asObject(raw);
1230
+ if (!obj) {
1231
+ return raw;
1232
+ }
1233
+ return {
1234
+ ...obj,
1235
+ page_num: coerceNumberLike(obj.page_num),
1236
+ page_size: coerceNumberLike(obj.page_size),
1237
+ requested_pages: coerceNumberLike(obj.requested_pages),
1238
+ scan_max_pages: coerceNumberLike(obj.scan_max_pages),
1239
+ type: coerceNumberLike(obj.type),
1240
+ max_rows: coerceNumberLike(obj.max_rows),
1241
+ max_items: coerceNumberLike(obj.max_items),
1242
+ max_columns: coerceNumberLike(obj.max_columns),
1243
+ strict_full: coerceBooleanLike(obj.strict_full),
1244
+ include_answers: coerceBooleanLike(obj.include_answers),
1245
+ apply_ids: normalizeIdArrayInput(obj.apply_ids),
1246
+ sort: normalizeSortInput(obj.sort),
1247
+ filters: normalizeFiltersInput(obj.filters),
1248
+ select_columns: normalizeSelectorListInput(obj.select_columns),
1249
+ time_range: normalizeTimeRangeInput(obj.time_range)
1250
+ };
1251
+ }
1252
+ function normalizeRecordGetInput(raw) {
1253
+ const obj = asObject(raw);
1254
+ if (!obj) {
1255
+ return raw;
1256
+ }
1257
+ return {
1258
+ ...obj,
1259
+ apply_id: coerceNumberLike(obj.apply_id),
1260
+ max_columns: coerceNumberLike(obj.max_columns),
1261
+ select_columns: normalizeSelectorListInput(obj.select_columns)
1262
+ };
1263
+ }
1264
+ function normalizeQueryInput(raw) {
1265
+ const obj = asObject(raw);
1266
+ if (!obj) {
1267
+ return raw;
1268
+ }
1269
+ return {
1270
+ ...obj,
1271
+ page_num: coerceNumberLike(obj.page_num),
1272
+ page_size: coerceNumberLike(obj.page_size),
1273
+ requested_pages: coerceNumberLike(obj.requested_pages),
1274
+ scan_max_pages: coerceNumberLike(obj.scan_max_pages),
1275
+ type: coerceNumberLike(obj.type),
1276
+ max_rows: coerceNumberLike(obj.max_rows),
1277
+ max_items: coerceNumberLike(obj.max_items),
1278
+ max_columns: coerceNumberLike(obj.max_columns),
1279
+ apply_id: coerceNumberLike(obj.apply_id),
1280
+ strict_full: coerceBooleanLike(obj.strict_full),
1281
+ include_answers: coerceBooleanLike(obj.include_answers),
1282
+ apply_ids: normalizeIdArrayInput(obj.apply_ids),
1283
+ sort: normalizeSortInput(obj.sort),
1284
+ filters: normalizeFiltersInput(obj.filters),
1285
+ select_columns: normalizeSelectorListInput(obj.select_columns),
1286
+ time_range: normalizeTimeRangeInput(obj.time_range)
1287
+ };
1288
+ }
1289
+ function normalizeAggregateInput(raw) {
1290
+ const obj = asObject(raw);
1291
+ if (!obj) {
1292
+ return raw;
1293
+ }
1294
+ return {
1295
+ ...obj,
1296
+ page_num: coerceNumberLike(obj.page_num),
1297
+ page_size: coerceNumberLike(obj.page_size),
1298
+ requested_pages: coerceNumberLike(obj.requested_pages),
1299
+ scan_max_pages: coerceNumberLike(obj.scan_max_pages),
1300
+ type: coerceNumberLike(obj.type),
1301
+ max_groups: coerceNumberLike(obj.max_groups),
1302
+ strict_full: coerceBooleanLike(obj.strict_full),
1303
+ group_by: normalizeSelectorListInput(obj.group_by),
1304
+ amount_column: coerceNumberLike(obj.amount_column),
1305
+ apply_ids: normalizeIdArrayInput(obj.apply_ids),
1306
+ sort: normalizeSortInput(obj.sort),
1307
+ filters: normalizeFiltersInput(obj.filters),
1308
+ time_range: normalizeTimeRangeInput(obj.time_range)
1309
+ };
1310
+ }
1311
+ function coerceNumberLike(value) {
1312
+ if (typeof value === "string") {
1313
+ const trimmed = value.trim();
1314
+ if (/^-?\d+(\.\d+)?$/.test(trimmed)) {
1315
+ const parsed = Number(trimmed);
1316
+ if (Number.isFinite(parsed)) {
1317
+ return parsed;
1318
+ }
1319
+ }
1320
+ }
1321
+ return value;
1322
+ }
1323
+ function coerceBooleanLike(value) {
1324
+ if (typeof value === "string") {
1325
+ const trimmed = value.trim().toLowerCase();
1326
+ if (trimmed === "true") {
1327
+ return true;
1328
+ }
1329
+ if (trimmed === "false") {
1330
+ return false;
1331
+ }
1332
+ }
1333
+ return value;
1334
+ }
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;
1351
+ }
1352
+ }
1353
+ function normalizeSelectorListInput(value) {
1354
+ const parsed = parseJsonLike(value);
1355
+ if (Array.isArray(parsed)) {
1356
+ return parsed.map((item) => coerceNumberLike(item));
1357
+ }
1358
+ if (typeof parsed === "string") {
1359
+ const trimmed = parsed.trim();
1360
+ if (!trimmed) {
1361
+ return parsed;
1362
+ }
1363
+ if (trimmed.includes(",")) {
1364
+ return trimmed
1365
+ .split(",")
1366
+ .map((item) => item.trim())
1367
+ .filter((item) => item.length > 0)
1368
+ .map((item) => coerceNumberLike(item));
1369
+ }
1370
+ return [coerceNumberLike(trimmed)];
1371
+ }
1372
+ if (parsed !== undefined && parsed !== null) {
1373
+ return [coerceNumberLike(parsed)];
1374
+ }
1375
+ return parsed;
1376
+ }
1377
+ function normalizeIdArrayInput(value) {
1378
+ const parsed = parseJsonLike(value);
1379
+ if (Array.isArray(parsed)) {
1380
+ return parsed.map((item) => coerceNumberLike(item));
1381
+ }
1382
+ if (typeof parsed === "string" && parsed.includes(",")) {
1383
+ return parsed
1384
+ .split(",")
1385
+ .map((item) => item.trim())
1386
+ .filter((item) => item.length > 0)
1387
+ .map((item) => coerceNumberLike(item));
1388
+ }
1389
+ return parsed;
1390
+ }
1391
+ function normalizeSortInput(value) {
1392
+ const parsed = parseJsonLike(value);
1393
+ if (!Array.isArray(parsed)) {
1394
+ return parsed;
1395
+ }
1396
+ return parsed.map((item) => {
1397
+ const obj = asObject(item);
1398
+ if (!obj) {
1399
+ return item;
1400
+ }
1401
+ return {
1402
+ ...obj,
1403
+ que_id: coerceNumberLike(obj.que_id),
1404
+ ascend: coerceBooleanLike(obj.ascend)
1405
+ };
1406
+ });
1407
+ }
1408
+ function normalizeFiltersInput(value) {
1409
+ const parsed = parseJsonLike(value);
1410
+ if (parsed === undefined || parsed === null) {
1411
+ return parsed;
1412
+ }
1413
+ const list = Array.isArray(parsed) ? parsed : [parsed];
1414
+ return list.map((item) => {
1415
+ const obj = asObject(item);
1416
+ if (!obj) {
1417
+ return item;
1418
+ }
1419
+ return {
1420
+ ...obj,
1421
+ que_id: coerceNumberLike(obj.que_id),
1422
+ scope: coerceNumberLike(obj.scope),
1423
+ search_options: normalizeIdArrayInput(obj.search_options)
1424
+ };
1425
+ });
1426
+ }
1427
+ function normalizeTimeRangeInput(value) {
1428
+ const parsed = parseJsonLike(value);
1429
+ const obj = asObject(parsed);
1430
+ if (!obj) {
1431
+ return parsed;
1432
+ }
1433
+ return {
1434
+ ...obj,
1435
+ column: coerceNumberLike(obj.column)
1436
+ };
1437
+ }
916
1438
  function resolveStartPage(pageNum, pageToken, appKey) {
917
1439
  if (!pageToken) {
918
1440
  return pageNum ?? 1;
@@ -988,10 +1510,18 @@ function resolveQueryMode(args) {
988
1510
  }
989
1511
  function buildListArgsFromQuery(args) {
990
1512
  if (!args.app_key) {
991
- throw new Error("app_key is required for list query");
1513
+ throw missingRequiredFieldError({
1514
+ field: "app_key",
1515
+ tool: "qf_query(list)",
1516
+ fixHint: "Provide app_key, for example: {\"query_mode\":\"list\",\"app_key\":\"21b3d559\",...}"
1517
+ });
992
1518
  }
993
1519
  if (!args.select_columns?.length) {
994
- throw new Error("select_columns is required for list query");
1520
+ throw missingRequiredFieldError({
1521
+ field: "select_columns",
1522
+ tool: "qf_query(list)",
1523
+ fixHint: "Provide select_columns as an array (<=10), for example: {\"select_columns\":[0,\"客户全称\",\"报价总金额\"]}"
1524
+ });
995
1525
  }
996
1526
  const filters = buildListFiltersFromQuery(args);
997
1527
  return listInputSchema.parse({
@@ -1047,10 +1577,18 @@ function appendTimeRangeFilter(inputFilters, timeRange) {
1047
1577
  }
1048
1578
  function buildRecordGetArgsFromQuery(args) {
1049
1579
  if (args.apply_id === undefined) {
1050
- throw new Error("apply_id is required for record query");
1580
+ throw missingRequiredFieldError({
1581
+ field: "apply_id",
1582
+ tool: "qf_query(record)",
1583
+ fixHint: "Provide apply_id, for example: {\"query_mode\":\"record\",\"apply_id\":\"497600278750478338\",...}"
1584
+ });
1051
1585
  }
1052
1586
  if (!args.select_columns?.length) {
1053
- throw new Error("select_columns is required for record query");
1587
+ throw missingRequiredFieldError({
1588
+ field: "select_columns",
1589
+ tool: "qf_query(record)",
1590
+ fixHint: "Provide select_columns as an array (<=10), for example: {\"select_columns\":[0,\"客户全称\"]}"
1591
+ });
1054
1592
  }
1055
1593
  return recordGetInputSchema.parse({
1056
1594
  apply_id: args.apply_id,
@@ -1059,6 +1597,20 @@ function buildRecordGetArgsFromQuery(args) {
1059
1597
  });
1060
1598
  }
1061
1599
  async function executeRecordsList(args) {
1600
+ if (!args.app_key) {
1601
+ throw missingRequiredFieldError({
1602
+ field: "app_key",
1603
+ tool: "qf_records_list",
1604
+ fixHint: "Provide app_key, for example: {\"app_key\":\"21b3d559\",...}"
1605
+ });
1606
+ }
1607
+ if (!args.select_columns?.length) {
1608
+ throw missingRequiredFieldError({
1609
+ field: "select_columns",
1610
+ tool: "qf_records_list",
1611
+ fixHint: "Provide select_columns as an array (<=10), for example: {\"select_columns\":[0,\"客户全称\",\"报价总金额\"]}"
1612
+ });
1613
+ }
1062
1614
  const queryId = randomUUID();
1063
1615
  const pageNum = resolveStartPage(args.page_num, args.page_token, args.app_key);
1064
1616
  const pageSize = args.page_size ?? DEFAULT_PAGE_SIZE;
@@ -1171,11 +1723,12 @@ async function executeRecordsList(args) {
1171
1723
  }
1172
1724
  : null
1173
1725
  };
1726
+ const evidence = buildEvidencePayload(listState, sourcePages);
1174
1727
  if (args.strict_full && !isComplete) {
1175
1728
  throw new NeedMoreDataError("List result is incomplete. Increase requested_pages/max_rows or continue with next_page_token.", {
1176
1729
  code: "NEED_MORE_DATA",
1177
1730
  completeness,
1178
- evidence: buildEvidencePayload(listState, sourcePages)
1731
+ evidence
1179
1732
  });
1180
1733
  }
1181
1734
  const responsePayload = {
@@ -1196,8 +1749,13 @@ async function executeRecordsList(args) {
1196
1749
  selected_columns: columnProjection.selectedColumns
1197
1750
  },
1198
1751
  completeness,
1199
- evidence: buildEvidencePayload(listState, sourcePages)
1752
+ evidence
1200
1753
  },
1754
+ completeness,
1755
+ evidence,
1756
+ error_code: null,
1757
+ fix_hint: null,
1758
+ next_page_token: completeness.next_page_token,
1201
1759
  meta: responseMeta
1202
1760
  };
1203
1761
  return {
@@ -1210,6 +1768,13 @@ async function executeRecordsList(args) {
1210
1768
  };
1211
1769
  }
1212
1770
  async function executeRecordGet(args) {
1771
+ if (!args.select_columns?.length) {
1772
+ throw missingRequiredFieldError({
1773
+ field: "select_columns",
1774
+ tool: "qf_record_get",
1775
+ fixHint: "Provide select_columns as an array (<=10), for example: {\"apply_id\":\"...\",\"select_columns\":[0]}"
1776
+ });
1777
+ }
1213
1778
  const queryId = randomUUID();
1214
1779
  const response = await client.getRecord(String(args.apply_id));
1215
1780
  const record = asObject(response.result) ?? {};
@@ -1253,6 +1818,27 @@ async function executeRecordGet(args) {
1253
1818
  selected_columns: projection.selectedColumns ?? []
1254
1819
  }
1255
1820
  },
1821
+ completeness: {
1822
+ result_amount: 1,
1823
+ returned_items: 1,
1824
+ fetched_pages: 1,
1825
+ requested_pages: 1,
1826
+ actual_scanned_pages: 1,
1827
+ has_more: false,
1828
+ next_page_token: null,
1829
+ is_complete: true,
1830
+ partial: false,
1831
+ omitted_items: 0,
1832
+ omitted_chars: 0
1833
+ },
1834
+ evidence: {
1835
+ query_id: queryId,
1836
+ apply_id: String(args.apply_id),
1837
+ selected_columns: projection.selectedColumns ?? []
1838
+ },
1839
+ error_code: null,
1840
+ fix_hint: null,
1841
+ next_page_token: null,
1256
1842
  meta: buildMeta(response)
1257
1843
  },
1258
1844
  message: `Fetched record ${String(args.apply_id)}`
@@ -1260,10 +1846,18 @@ async function executeRecordGet(args) {
1260
1846
  }
1261
1847
  async function executeRecordsSummary(args) {
1262
1848
  if (!args.app_key) {
1263
- throw new Error("app_key is required for summary query");
1849
+ throw missingRequiredFieldError({
1850
+ field: "app_key",
1851
+ tool: "qf_query(summary)",
1852
+ fixHint: "Provide app_key, for example: {\"query_mode\":\"summary\",\"app_key\":\"21b3d559\",...}"
1853
+ });
1264
1854
  }
1265
1855
  if (!args.select_columns?.length) {
1266
- throw new Error("select_columns is required for summary query");
1856
+ throw missingRequiredFieldError({
1857
+ field: "select_columns",
1858
+ tool: "qf_query(summary)",
1859
+ fixHint: "Provide select_columns as an array (<=10), for example: {\"select_columns\":[\"客户全称\"]}"
1860
+ });
1267
1861
  }
1268
1862
  const queryId = randomUUID();
1269
1863
  const strictFull = args.strict_full ?? true;
@@ -1705,6 +2299,11 @@ async function executeRecordsAggregate(args) {
1705
2299
  }
1706
2300
  }
1707
2301
  },
2302
+ completeness,
2303
+ evidence,
2304
+ error_code: null,
2305
+ fix_hint: null,
2306
+ next_page_token: completeness.next_page_token,
1708
2307
  meta: responseMeta
1709
2308
  },
1710
2309
  message: isComplete
@@ -1773,12 +2372,19 @@ function extractSummaryColumnValue(answers, column) {
1773
2372
  }
1774
2373
  function extractAnswerDisplayValue(answer) {
1775
2374
  const tableValues = answer.tableValues ?? answer.table_values;
1776
- if (tableValues !== undefined) {
2375
+ if (Array.isArray(tableValues)) {
2376
+ // Qingflow often sends tableValues: [] for non-table fields.
2377
+ // Prefer non-empty tableValues; otherwise fallback to values.
2378
+ if (tableValues.length > 0) {
2379
+ return tableValues;
2380
+ }
2381
+ }
2382
+ else if (tableValues !== undefined && tableValues !== null) {
1777
2383
  return tableValues;
1778
2384
  }
1779
2385
  const values = asArray(answer.values);
1780
2386
  if (values.length === 0) {
1781
- return null;
2387
+ return Array.isArray(tableValues) ? tableValues : null;
1782
2388
  }
1783
2389
  const normalized = values.map((item) => extractAnswerValueCell(item));
1784
2390
  return normalized.length === 1 ? normalized[0] : normalized;
@@ -2351,40 +2957,69 @@ function errorResult(error) {
2351
2957
  }
2352
2958
  function toErrorPayload(error) {
2353
2959
  if (error instanceof NeedMoreDataError) {
2960
+ const details = asObject(error.details);
2961
+ const completeness = asObject(details?.completeness);
2354
2962
  return {
2355
2963
  ok: false,
2356
2964
  code: error.code,
2965
+ error_code: error.code,
2357
2966
  status: "need_more_data",
2358
2967
  message: error.message,
2968
+ fix_hint: "Continue with next_page_token or increase requested_pages/scan_max_pages.",
2969
+ next_page_token: asNullableString(completeness?.next_page_token),
2970
+ details: error.details
2971
+ };
2972
+ }
2973
+ if (error instanceof InputValidationError) {
2974
+ return {
2975
+ ok: false,
2976
+ error_code: error.errorCode,
2977
+ message: error.message,
2978
+ fix_hint: error.fixHint,
2979
+ next_page_token: null,
2359
2980
  details: error.details
2360
2981
  };
2361
2982
  }
2362
2983
  if (error instanceof QingflowApiError) {
2363
2984
  return {
2364
2985
  ok: false,
2986
+ error_code: "QINGFLOW_API_ERROR",
2365
2987
  message: error.message,
2366
2988
  err_code: error.errCode,
2367
2989
  err_msg: error.errMsg || null,
2368
2990
  http_status: error.httpStatus,
2991
+ fix_hint: "Check app_key/accessToken and request body against qf_form_get field definitions.",
2992
+ next_page_token: null,
2369
2993
  details: error.details ?? null
2370
2994
  };
2371
2995
  }
2372
2996
  if (error instanceof z.ZodError) {
2997
+ const firstIssue = error.issues[0];
2998
+ const firstPath = firstIssue?.path?.join(".") || "arguments";
2373
2999
  return {
2374
3000
  ok: false,
3001
+ error_code: "INVALID_ARGUMENTS",
2375
3002
  message: "Invalid arguments",
3003
+ fix_hint: `Fix field "${firstPath}" and retry with schema-compliant values.`,
3004
+ next_page_token: null,
2376
3005
  issues: error.issues
2377
3006
  };
2378
3007
  }
2379
3008
  if (error instanceof Error) {
2380
3009
  return {
2381
3010
  ok: false,
2382
- message: error.message
3011
+ error_code: "INTERNAL_ERROR",
3012
+ message: error.message,
3013
+ fix_hint: "Retry the request. If it persists, report query_id and input payload.",
3014
+ next_page_token: null
2383
3015
  };
2384
3016
  }
2385
3017
  return {
2386
3018
  ok: false,
3019
+ error_code: "UNKNOWN_ERROR",
2387
3020
  message: "Unknown error",
3021
+ fix_hint: "Retry the request with explicit app_key/select_columns and deterministic page parameters.",
3022
+ next_page_token: null,
2388
3023
  details: error
2389
3024
  };
2390
3025
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qingflow-mcp",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "private": false,
5
5
  "license": "MIT",
6
6
  "type": "module",