qingflow-mcp 0.4.0 → 0.4.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.
package/README.md CHANGED
@@ -5,6 +5,7 @@ This MCP server wraps Qingflow OpenAPI for:
5
5
  - `qf_apps_list`
6
6
  - `qf_form_get`
7
7
  - `qf_field_resolve`
8
+ - `qf_value_probe`
8
9
  - `qf_query_plan`
9
10
  - `qf_records_list`
10
11
  - `qf_record_get`
@@ -108,7 +109,7 @@ npm i -g git+https://github.com/853046310/qingflow-mcp.git
108
109
  Install from npm (pinned version):
109
110
 
110
111
  ```bash
111
- npm i -g qingflow-mcp@0.4.0
112
+ npm i -g qingflow-mcp@0.4.2
112
113
  ```
113
114
 
114
115
  Or one-click installer:
@@ -145,8 +146,10 @@ MCP client config example:
145
146
 
146
147
  1. `qf_apps_list` to pick app.
147
148
  2. `qf_form_get` to inspect field ids/titles.
148
- 3. `qf_record_create` or `qf_record_update`.
149
- 4. If create/update returns only `request_id`, call `qf_operation_get` to resolve async result.
149
+ 3. `qf_field_resolve` for field-name to `que_id` mapping.
150
+ 4. `qf_value_probe` when the agent needs candidate field values and explicit match evidence.
151
+ 5. `qf_record_create` or `qf_record_update`.
152
+ 6. If create/update returns only `request_id`, call `qf_operation_get` to resolve async result.
150
153
 
151
154
  Full calling contract (Chinese):
152
155
 
@@ -214,7 +217,7 @@ Deterministic read protocol (list/summary/aggregate):
214
217
  - objects must be native JSON objects
215
218
  - booleans must be native JSON booleans
216
219
  - unknown fields are rejected by the MCP boundary
217
- 7. Use `qf_query_plan` as the only preflight tool when the agent is unsure about arguments. It can normalize loose/model-shaped inputs before a real query is issued.
220
+ 7. Use `qf_query_plan` as the only preflight tool when the agent is unsure about arguments. It can normalize loose/model-shaped inputs before a real query is issued, but runtime aliases like `from`/`to`/`dateFrom`/`dateTo`/`searchKey`/`searchKeys` are rejected.
218
221
 
219
222
  For `qf_query(summary)` and `qf_records_aggregate`, read `data.summary.completeness` / `data.completeness` before concluding:
220
223
 
@@ -1,3 +1,5 @@
1
+ import { request as httpRequest } from "node:http";
2
+ import { request as httpsRequest } from "node:https";
1
3
  export class QingflowApiError extends Error {
2
4
  errCode;
3
5
  errMsg;
@@ -94,25 +96,25 @@ export class QingflowClient {
94
96
  if (options.userId) {
95
97
  headers.userId = options.userId;
96
98
  }
97
- const init = {
98
- method: params.method,
99
- headers
100
- };
99
+ let requestBody;
101
100
  if (options.body !== undefined) {
102
101
  headers["content-type"] = "application/json";
103
- init.body = JSON.stringify(options.body);
102
+ requestBody = JSON.stringify(options.body);
103
+ headers["content-length"] = String(Buffer.byteLength(requestBody));
104
104
  }
105
- const controller = new AbortController();
106
- const timer = setTimeout(() => controller.abort(), this.timeoutMs);
107
- init.signal = controller.signal;
108
105
  try {
109
- const response = await getFetch()(url, init);
110
- const text = await response.text();
106
+ const response = await sendHttpRequest(url, {
107
+ method: params.method,
108
+ headers,
109
+ body: requestBody,
110
+ timeoutMs: this.timeoutMs
111
+ });
112
+ const text = response.text;
111
113
  const data = safeJsonParse(text);
112
- if (!response.ok) {
114
+ if (response.statusCode < 200 || response.statusCode >= 300) {
113
115
  throw new QingflowApiError({
114
- message: `Qingflow HTTP ${response.status}`,
115
- httpStatus: response.status,
116
+ message: `Qingflow HTTP ${response.statusCode}`,
117
+ httpStatus: response.statusCode,
116
118
  errMsg: extractErrMsg(data, text),
117
119
  details: data ?? text
118
120
  });
@@ -120,7 +122,7 @@ export class QingflowClient {
120
122
  if (!data || typeof data !== "object") {
121
123
  throw new QingflowApiError({
122
124
  message: "Qingflow response is not JSON object",
123
- httpStatus: response.status,
125
+ httpStatus: response.statusCode,
124
126
  details: text
125
127
  });
126
128
  }
@@ -128,7 +130,7 @@ export class QingflowClient {
128
130
  if (parsed.errCode === null) {
129
131
  throw new QingflowApiError({
130
132
  message: "Qingflow response missing code field",
131
- httpStatus: response.status,
133
+ httpStatus: response.statusCode,
132
134
  details: data
133
135
  });
134
136
  }
@@ -137,7 +139,7 @@ export class QingflowClient {
137
139
  message: `Qingflow API error ${parsed.errCode}: ${parsed.errMsg}`,
138
140
  errCode: parsed.errCode,
139
141
  errMsg: parsed.errMsg,
140
- httpStatus: response.status,
142
+ httpStatus: response.statusCode,
141
143
  details: data
142
144
  });
143
145
  }
@@ -151,9 +153,9 @@ export class QingflowClient {
151
153
  if (error instanceof QingflowApiError) {
152
154
  throw error;
153
155
  }
154
- if (error instanceof Error && error.name === "AbortError") {
156
+ if (error instanceof Error && error.message.startsWith("Qingflow request timeout after")) {
155
157
  throw new QingflowApiError({
156
- message: `Qingflow request timeout after ${this.timeoutMs}ms`
158
+ message: error.message
157
159
  });
158
160
  }
159
161
  throw new QingflowApiError({
@@ -161,9 +163,6 @@ export class QingflowClient {
161
163
  details: error
162
164
  });
163
165
  }
164
- finally {
165
- clearTimeout(timer);
166
- }
167
166
  }
168
167
  }
169
168
  function normalizeBaseUrl(url) {
@@ -254,12 +253,31 @@ function toFiniteNumber(value) {
254
253
  }
255
254
  return null;
256
255
  }
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."
256
+ async function sendHttpRequest(url, params) {
257
+ const requestImpl = url.protocol === "https:" ? httpsRequest : httpRequest;
258
+ return await new Promise((resolve, reject) => {
259
+ const req = requestImpl(url, {
260
+ method: params.method,
261
+ headers: params.headers
262
+ }, (res) => {
263
+ const chunks = [];
264
+ res.on("data", (chunk) => {
265
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
266
+ });
267
+ res.on("end", () => {
268
+ resolve({
269
+ statusCode: res.statusCode ?? 0,
270
+ text: Buffer.concat(chunks).toString("utf8")
271
+ });
272
+ });
273
+ });
274
+ req.setTimeout(params.timeoutMs, () => {
275
+ req.destroy(new Error(`Qingflow request timeout after ${params.timeoutMs}ms`));
276
+ });
277
+ req.on("error", reject);
278
+ if (params.body !== undefined) {
279
+ req.write(params.body);
280
+ }
281
+ req.end();
264
282
  });
265
283
  }
package/dist/server.js CHANGED
@@ -66,7 +66,7 @@ const ADAPTIVE_TARGET_PAGE_MS = toPositiveInt(process.env.QINGFLOW_ADAPTIVE_TARG
66
66
  const MAX_LIST_ITEMS_BYTES = toPositiveInt(process.env.QINGFLOW_LIST_MAX_ITEMS_BYTES) ?? 400000;
67
67
  const REQUEST_TIMEOUT_MS = toPositiveInt(process.env.QINGFLOW_REQUEST_TIMEOUT_MS) ?? 18000;
68
68
  const EXECUTION_BUDGET_MS = toPositiveInt(process.env.QINGFLOW_EXECUTION_BUDGET_MS) ?? 20000;
69
- const SERVER_VERSION = "0.4.0";
69
+ const SERVER_VERSION = "0.4.2";
70
70
  const accessToken = process.env.QINGFLOW_ACCESS_TOKEN;
71
71
  const baseUrl = process.env.QINGFLOW_BASE_URL;
72
72
  if (!accessToken) {
@@ -137,6 +137,8 @@ const apiMetaSchema = z.object({
137
137
  base_url: z.string()
138
138
  });
139
139
  const outputProfileSchema = z.enum(["compact", "verbose"]);
140
+ const matchModeValues = ["exact", "normalized", "contains", "prefix", "fuzzy"];
141
+ const matchModeSchema = z.enum(matchModeValues);
140
142
  const completenessSchema = z.object({
141
143
  result_amount: z.number().int().nonnegative(),
142
144
  returned_items: z.number().int().nonnegative(),
@@ -156,7 +158,12 @@ const completenessSchema = z.object({
156
158
  output_page_complete: z.boolean().optional(),
157
159
  raw_next_page_token: z.string().nullable().optional(),
158
160
  output_next_page_token: z.string().nullable().optional(),
159
- stop_reason: z.string().nullable().optional()
161
+ stop_reason: z.string().nullable().optional(),
162
+ provider_result_amount: z.number().int().nonnegative().nullable().optional(),
163
+ source_record_count: z.number().int().nonnegative().optional(),
164
+ effective_record_count: z.number().int().nonnegative().optional(),
165
+ returned_group_count: z.number().int().nonnegative().optional(),
166
+ total_group_count: z.number().int().nonnegative().optional()
160
167
  });
161
168
  const evidenceSchema = z.object({
162
169
  query_id: z.string(),
@@ -237,7 +244,7 @@ const publicFieldSelectorSchema = z.union([publicStringSchema, z.number().int()]
237
244
  const publicSortItemSchema = z.object({
238
245
  que_id: publicFieldSelectorSchema,
239
246
  ascend: z.boolean().optional()
240
- });
247
+ }).strict();
241
248
  const publicFilterItemSchema = z.object({
242
249
  que_id: publicFieldSelectorSchema.optional(),
243
250
  search_key: publicStringSchema.optional(),
@@ -247,17 +254,17 @@ const publicFilterItemSchema = z.object({
247
254
  scope: z.number().int().optional(),
248
255
  search_options: z.array(publicFieldSelectorSchema).optional(),
249
256
  search_user_ids: z.array(publicStringSchema).optional()
250
- });
257
+ }).strict();
251
258
  const publicTimeRangeSchema = z.object({
252
259
  column: publicFieldSelectorSchema,
253
260
  from: publicStringSchema.optional(),
254
261
  to: publicStringSchema.optional(),
255
262
  timezone: publicStringSchema.optional()
256
- });
263
+ }).strict();
257
264
  const publicStatPolicySchema = z.object({
258
265
  include_negative: z.boolean().optional(),
259
266
  include_null: z.boolean().optional()
260
- });
267
+ }).strict();
261
268
  const publicAnswerInputSchema = z.object({
262
269
  que_id: publicFieldSelectorSchema.optional(),
263
270
  queId: publicFieldSelectorSchema.optional(),
@@ -342,7 +349,8 @@ const listInputPublicSchema = z
342
349
  include_answers: z.boolean().optional(),
343
350
  strict_full: z.boolean().optional(),
344
351
  output_profile: outputProfileSchema.optional()
345
- });
352
+ })
353
+ .strict();
346
354
  const listInputSchema = z
347
355
  .preprocess(normalizeListInput, z.object({
348
356
  app_key: z.string().min(1).optional(),
@@ -603,7 +611,8 @@ const queryInputPublicSchema = z
603
611
  scan_max_pages: z.number().int().positive().max(500).optional(),
604
612
  strict_full: z.boolean().optional(),
605
613
  output_profile: outputProfileSchema.optional()
606
- });
614
+ })
615
+ .strict();
607
616
  const queryInputSchema = z
608
617
  .preprocess(normalizeQueryInput, z.object({
609
618
  query_mode: z.enum(["auto", "list", "record", "summary"]).optional(),
@@ -785,7 +794,8 @@ const aggregateInputPublicSchema = z
785
794
  max_groups: z.number().int().positive().max(2000).optional(),
786
795
  strict_full: z.boolean().optional(),
787
796
  output_profile: outputProfileSchema.optional()
788
- });
797
+ })
798
+ .strict();
789
799
  const aggregateInputSchema = z
790
800
  .preprocess(normalizeAggregateInput, z.object({
791
801
  app_key: z.string().min(1),
@@ -936,6 +946,60 @@ const fieldResolveOutputSchema = z.object({
936
946
  }),
937
947
  meta: apiMetaSchema
938
948
  });
949
+ const valueProbeInputPublicSchema = z
950
+ .object({
951
+ app_key: publicStringSchema,
952
+ field: publicFieldSelectorSchema,
953
+ query: publicStringSchema.optional(),
954
+ match_mode: matchModeSchema.optional(),
955
+ limit: z.number().int().positive().max(50).optional(),
956
+ scan_max_pages: z.number().int().positive().max(20).optional(),
957
+ page_size: z.number().int().positive().max(200).optional()
958
+ })
959
+ .strict();
960
+ const valueProbeInputSchema = z.preprocess(normalizeValueProbeInput, z
961
+ .object({
962
+ app_key: z.string().min(1),
963
+ field: z.union([z.string().min(1), z.number().int()]),
964
+ query: z.string().optional(),
965
+ match_mode: matchModeSchema.optional(),
966
+ limit: z.number().int().positive().max(50).optional(),
967
+ scan_max_pages: z.number().int().positive().max(20).optional(),
968
+ page_size: z.number().int().positive().max(200).optional()
969
+ })
970
+ .strict());
971
+ const valueProbeOutputSchema = z.object({
972
+ ok: z.literal(true),
973
+ data: z.object({
974
+ app_key: z.string(),
975
+ field: z.object({
976
+ requested: z.string(),
977
+ que_id: z.union([z.string(), z.number()]),
978
+ que_title: z.string().nullable(),
979
+ que_type: z.unknown()
980
+ }),
981
+ query: z.string().nullable(),
982
+ requested_match_mode: matchModeSchema,
983
+ effective_match_mode: matchModeSchema,
984
+ provider_translation: z.string(),
985
+ scanned_pages: z.number().int().nonnegative(),
986
+ scanned_records: z.number().int().nonnegative(),
987
+ provider_result_amount: z.number().int().nonnegative().nullable(),
988
+ candidates: z.array(z.object({
989
+ value: z.unknown(),
990
+ display_value: z.string(),
991
+ count: z.number().int().nonnegative(),
992
+ match_strength: z.number().min(0).max(1),
993
+ matched_texts: z.array(z.string()),
994
+ matched_as: z.string()
995
+ })),
996
+ matched_values: z.array(z.unknown())
997
+ }),
998
+ meta: z.object({
999
+ version: z.string(),
1000
+ generated_at: z.string()
1001
+ })
1002
+ });
939
1003
  const queryPlanInputPublicSchema = z
940
1004
  .object({
941
1005
  tool: publicStringSchema,
@@ -1072,7 +1136,8 @@ const exportInputPublicSchema = z
1072
1136
  output_profile: outputProfileSchema.optional(),
1073
1137
  export_dir: publicStringSchema.optional(),
1074
1138
  file_name: publicStringSchema.optional()
1075
- });
1139
+ })
1140
+ .strict();
1076
1141
  const exportInputSchema = z.preprocess(normalizeExportInput, z.object({
1077
1142
  app_key: z.string().min(1).optional(),
1078
1143
  user_id: z.string().min(1).optional(),
@@ -1168,13 +1233,7 @@ const exportOutputSchema = z.object({
1168
1233
  meta: apiMetaSchema.optional()
1169
1234
  });
1170
1235
  const canonicalFieldRefSchema = publicFieldSelectorSchema;
1171
- const canonicalMatchModeSchema = z.enum([
1172
- "exact",
1173
- "normalized",
1174
- "contains",
1175
- "prefix",
1176
- "fuzzy"
1177
- ]);
1236
+ const canonicalMatchModeSchema = matchModeSchema;
1178
1237
  const canonicalWhereOpSchema = z.enum([
1179
1238
  "eq",
1180
1239
  "neq",
@@ -1199,6 +1258,7 @@ const canonicalWhereItemSchema = z
1199
1258
  to: z.union([z.string(), z.number()]).optional(),
1200
1259
  match: canonicalMatchModeSchema.optional()
1201
1260
  })
1261
+ .strict()
1202
1262
  .superRefine((value, ctx) => {
1203
1263
  if (value.op === "between") {
1204
1264
  if (value.from === undefined && value.to === undefined) {
@@ -1228,13 +1288,16 @@ const canonicalWhereItemSchema = z
1228
1288
  });
1229
1289
  }
1230
1290
  });
1231
- const canonicalSortItemSchema = z.object({
1291
+ const canonicalSortItemSchema = z
1292
+ .object({
1232
1293
  field: canonicalFieldRefSchema,
1233
1294
  direction: canonicalSortDirectionSchema.optional()
1234
- });
1295
+ })
1296
+ .strict();
1235
1297
  const canonicalSelectSchema = z.array(canonicalFieldRefSchema).min(1).max(MAX_COLUMN_LIMIT);
1236
1298
  const canonicalGroupBySchema = z.array(canonicalFieldRefSchema).min(1).max(20);
1237
- const canonicalBaseQueryInputSchema = z.object({
1299
+ const canonicalBaseQueryInputSchema = z
1300
+ .object({
1238
1301
  app_key: z.string().min(1),
1239
1302
  select: canonicalSelectSchema.optional(),
1240
1303
  where: z.array(canonicalWhereItemSchema).optional(),
@@ -1263,27 +1326,35 @@ const canonicalBaseQueryInputSchema = z.object({
1263
1326
  "cc"
1264
1327
  ])
1265
1328
  .optional()
1266
- });
1267
- const canonicalPlanInputSchema = z.object({
1329
+ })
1330
+ .strict();
1331
+ const canonicalPlanInputSchema = z
1332
+ .object({
1268
1333
  kind: z.enum(["rows", "record", "aggregate", "export", "mutate"]),
1269
1334
  query: z.record(z.unknown()).optional(),
1270
1335
  action: z.record(z.unknown()).optional(),
1271
1336
  probe: z.boolean().optional(),
1272
1337
  resolve_fields: z.boolean().optional()
1273
- });
1274
- const canonicalRowsInputSchema = canonicalBaseQueryInputSchema.extend({
1338
+ })
1339
+ .strict();
1340
+ const canonicalRowsInputSchema = canonicalBaseQueryInputSchema
1341
+ .extend({
1275
1342
  select: canonicalSelectSchema
1276
- });
1277
- const canonicalRecordInputSchema = z.object({
1343
+ })
1344
+ .strict();
1345
+ const canonicalRecordInputSchema = z
1346
+ .object({
1278
1347
  apply_id: canonicalFieldRefSchema,
1279
1348
  select: canonicalSelectSchema,
1280
1349
  output_profile: outputProfileSchema.optional()
1281
- });
1350
+ })
1351
+ .strict();
1282
1352
  const canonicalAggregateMetricInputSchema = z
1283
1353
  .object({
1284
1354
  column: canonicalFieldRefSchema.optional(),
1285
1355
  op: z.enum(["count", "sum", "avg", "min", "max"])
1286
1356
  })
1357
+ .strict()
1287
1358
  .superRefine((value, ctx) => {
1288
1359
  if (value.op !== "count" && value.column === undefined) {
1289
1360
  ctx.addIssue({
@@ -1292,13 +1363,16 @@ const canonicalAggregateMetricInputSchema = z
1292
1363
  });
1293
1364
  }
1294
1365
  });
1295
- const canonicalAggregateInputSchema = canonicalBaseQueryInputSchema.extend({
1366
+ const canonicalAggregateInputSchema = canonicalBaseQueryInputSchema
1367
+ .extend({
1296
1368
  group_by: canonicalGroupBySchema,
1297
1369
  metrics: z.array(canonicalAggregateMetricInputSchema).min(1).max(10),
1298
1370
  time_bucket: z.enum(["day", "week", "month"]).optional(),
1299
1371
  top_n: z.number().int().positive().max(2000).optional()
1300
- });
1301
- const canonicalMutateInputSchema = z.object({
1372
+ })
1373
+ .strict();
1374
+ const canonicalMutateInputSchema = z
1375
+ .object({
1302
1376
  action: z.enum(["create", "update"]),
1303
1377
  app_key: z.string().min(1).optional(),
1304
1378
  apply_id: canonicalFieldRefSchema.optional(),
@@ -1306,24 +1380,33 @@ const canonicalMutateInputSchema = z.object({
1306
1380
  fields: z.record(z.unknown()).optional(),
1307
1381
  answers: z.array(publicAnswerInputSchema).optional(),
1308
1382
  force_refresh_form: z.boolean().optional()
1309
- });
1310
- const canonicalExportInputSchema = canonicalBaseQueryInputSchema.extend({
1383
+ })
1384
+ .strict();
1385
+ const canonicalExportInputSchema = canonicalBaseQueryInputSchema
1386
+ .extend({
1311
1387
  select: canonicalSelectSchema,
1312
1388
  format: z.enum(["csv", "json"]).optional(),
1313
1389
  file_name: z.string().min(1).optional(),
1314
1390
  export_dir: z.string().min(1).optional()
1315
- });
1391
+ })
1392
+ .strict();
1316
1393
  const canonicalResourceRefSchema = z.object({
1317
1394
  uri: z.string(),
1318
1395
  name: z.string(),
1319
1396
  mime_type: z.string(),
1320
1397
  description: z.string().nullable().optional()
1321
1398
  });
1399
+ const canonicalExecutionPlanSchema = z.object({
1400
+ tool: z.string().nullable(),
1401
+ arguments: z.record(z.unknown()).nullable(),
1402
+ direct_execute: z.boolean()
1403
+ });
1322
1404
  const canonicalPlanOutputSchema = z.object({
1323
1405
  ok: z.literal(true),
1324
1406
  data: z.object({
1325
1407
  kind: z.string(),
1326
1408
  normalized_query: z.record(z.unknown()),
1409
+ plan: canonicalExecutionPlanSchema,
1327
1410
  internal_tool: z.string().nullable(),
1328
1411
  internal_arguments: z.record(z.unknown()).nullable(),
1329
1412
  field_mapping: z.array(z.record(z.unknown())),
@@ -1382,6 +1465,9 @@ const canonicalAggregateOutputSchema = z.object({
1382
1465
  primary_metric_column: z.string().nullable(),
1383
1466
  summary: z.object({
1384
1467
  record_count: z.number().int().nonnegative(),
1468
+ provider_result_amount: z.number().int().nonnegative().nullable(),
1469
+ returned_group_count: z.number().int().nonnegative(),
1470
+ total_group_count: z.number().int().nonnegative(),
1385
1471
  metrics_by_column: z.record(z.record(z.union([z.number(), z.null()])))
1386
1472
  }),
1387
1473
  groups: z.array(z.record(z.unknown())),
@@ -1567,6 +1653,25 @@ server.registerTool("qf_field_resolve", {
1567
1653
  return errorResult(error);
1568
1654
  }
1569
1655
  });
1656
+ server.registerTool("qf_value_probe", {
1657
+ title: "Qingflow Value Probe",
1658
+ description: "Probe likely field values for one app field, with explicit match mode and matched value evidence.",
1659
+ inputSchema: valueProbeInputPublicSchema,
1660
+ outputSchema: valueProbeOutputSchema,
1661
+ annotations: {
1662
+ readOnlyHint: true,
1663
+ idempotentHint: true
1664
+ }
1665
+ }, async (args) => {
1666
+ try {
1667
+ const parsedArgs = valueProbeInputSchema.parse(args);
1668
+ const payload = await executeValueProbe(parsedArgs);
1669
+ return okResult(payload, `Probed values for ${parsedArgs.app_key}:${String(parsedArgs.field)}`);
1670
+ }
1671
+ catch (error) {
1672
+ return errorResult(error);
1673
+ }
1674
+ });
1570
1675
  server.registerTool("qf_query_plan", {
1571
1676
  title: "Qingflow Query Plan",
1572
1677
  description: "Preflight query arguments: normalize inputs, validate required fields, resolve mappings and estimate scan limits before execution.",
@@ -1598,6 +1703,7 @@ server.registerTool("qf.query.plan", {
1598
1703
  }, async (args) => {
1599
1704
  try {
1600
1705
  const parsedArgs = canonicalPlanInputSchema.parse(args);
1706
+ const canonicalTool = canonicalExecutorToolForKind(parsedArgs.kind);
1601
1707
  const normalizedLoose = normalizeCanonicalLooseInput(parsedArgs.kind, parsedArgs.kind === "mutate" ? parsedArgs.action : parsedArgs.query);
1602
1708
  let normalizedQuery = normalizedLoose;
1603
1709
  let internalTool = parsedArgs.kind === "rows"
@@ -1664,6 +1770,11 @@ server.registerTool("qf.query.plan", {
1664
1770
  data: {
1665
1771
  kind: parsedArgs.kind,
1666
1772
  normalized_query: normalizedQuery,
1773
+ plan: {
1774
+ tool: canonicalTool,
1775
+ arguments: normalizedQuery,
1776
+ direct_execute: false
1777
+ },
1667
1778
  internal_tool: internalTool,
1668
1779
  internal_arguments: internalArguments,
1669
1780
  field_mapping: [],
@@ -1700,6 +1811,13 @@ server.registerTool("qf.query.plan", {
1700
1811
  data: {
1701
1812
  kind: parsedArgs.kind,
1702
1813
  normalized_query: normalizedQuery,
1814
+ plan: {
1815
+ tool: canonicalTool,
1816
+ arguments: normalizedQuery,
1817
+ direct_execute: parsedArgs.kind === "mutate"
1818
+ ? true
1819
+ : Boolean(plannedPayload?.data.validation.valid && internalArguments)
1820
+ },
1703
1821
  internal_tool: internalTool,
1704
1822
  internal_arguments: internalArguments,
1705
1823
  field_mapping: plannedPayload?.data.field_mapping ?? [],
@@ -1912,6 +2030,9 @@ server.registerTool("qf.query.aggregate", {
1912
2030
  metrics_by_column: asObject(group.metrics) ?? {}
1913
2031
  };
1914
2032
  });
2033
+ const providerResultAmount = toNonNegativeInt(completeness.provider_result_amount ?? completeness.result_amount);
2034
+ const returnedGroupCount = toNonNegativeInt(completeness.returned_group_count) ?? groups.length;
2035
+ const totalGroupCount = toNonNegativeInt(completeness.total_group_count) ?? groups.length;
1915
2036
  const snapshotUri = buildQuerySnapshotResourceUri(queryId);
1916
2037
  const normalizedUri = buildQueryNormalizedResourceUri(queryId);
1917
2038
  const snapshot = {
@@ -1921,6 +2042,9 @@ server.registerTool("qf.query.aggregate", {
1921
2042
  primary_metric_column: built.primaryMetricColumn,
1922
2043
  summary: {
1923
2044
  record_count: toNonNegativeInt(data.summary.total_count) ?? 0,
2045
+ provider_result_amount: providerResultAmount,
2046
+ returned_group_count: returnedGroupCount,
2047
+ total_group_count: totalGroupCount,
1924
2048
  metrics_by_column: summaryMetrics
1925
2049
  },
1926
2050
  groups,
@@ -1948,6 +2072,9 @@ server.registerTool("qf.query.aggregate", {
1948
2072
  primary_metric_column: built.primaryMetricColumn,
1949
2073
  summary: {
1950
2074
  record_count: toNonNegativeInt(data.summary.total_count) ?? 0,
2075
+ provider_result_amount: providerResultAmount,
2076
+ returned_group_count: returnedGroupCount,
2077
+ total_group_count: totalGroupCount,
1951
2078
  metrics_by_column: summaryMetrics
1952
2079
  },
1953
2080
  groups,
@@ -3017,6 +3144,88 @@ function applyAliases(obj, aliases) {
3017
3144
  }
3018
3145
  return out;
3019
3146
  }
3147
+ const FORBIDDEN_FILTER_RUNTIME_ALIASES = {
3148
+ from: "min_value",
3149
+ to: "max_value",
3150
+ dateFrom: "min_value",
3151
+ dateTo: "max_value",
3152
+ searchKey: "search_key",
3153
+ searchKeys: "search_keys"
3154
+ };
3155
+ const FORBIDDEN_TOP_LEVEL_TIME_ALIASES = {
3156
+ from: "time_range.from",
3157
+ to: "time_range.to",
3158
+ dateFrom: "time_range.from",
3159
+ dateTo: "time_range.to"
3160
+ };
3161
+ function throwForbiddenRuntimeAliasError(params) {
3162
+ throw new InputValidationError({
3163
+ message: `${params.tool} no longer accepts runtime alias "${params.alias}" at ${params.path}`,
3164
+ errorCode: "FORBIDDEN_RUNTIME_ALIAS",
3165
+ fixHint: params.fixHint,
3166
+ details: {
3167
+ tool: params.tool,
3168
+ path: params.path,
3169
+ alias: params.alias,
3170
+ replacement: params.replacement
3171
+ }
3172
+ });
3173
+ }
3174
+ function assertNoForbiddenAliases(obj, aliasMap, params) {
3175
+ for (const [alias, replacement] of Object.entries(aliasMap)) {
3176
+ if (obj[alias] !== undefined) {
3177
+ throwForbiddenRuntimeAliasError({
3178
+ tool: params.tool,
3179
+ path: `${params.pathPrefix}.${alias}`,
3180
+ alias,
3181
+ replacement,
3182
+ fixHint: params.fixHint
3183
+ });
3184
+ }
3185
+ }
3186
+ }
3187
+ function assertNoLegacyFilterAliases(value, tool) {
3188
+ const parsed = parseJsonLikeDeep(value);
3189
+ const list = Array.isArray(parsed) ? parsed : parsed === undefined || parsed === null ? [] : [parsed];
3190
+ for (let index = 0; index < list.length; index += 1) {
3191
+ const item = list[index];
3192
+ const obj = asObject(item);
3193
+ if (!obj) {
3194
+ continue;
3195
+ }
3196
+ assertNoForbiddenAliases(obj, FORBIDDEN_FILTER_RUNTIME_ALIASES, {
3197
+ tool,
3198
+ pathPrefix: `filters[${index}]`,
3199
+ fixHint: 'Use legacy filter keys "min_value"/"max_value"/"search_key"/"search_keys", or switch to canonical qf.query.* where clauses.'
3200
+ });
3201
+ const valueObject = asObject(parseJsonLikeDeep(obj.value));
3202
+ if (!valueObject) {
3203
+ continue;
3204
+ }
3205
+ assertNoForbiddenAliases(valueObject, FORBIDDEN_FILTER_RUNTIME_ALIASES, {
3206
+ tool,
3207
+ pathPrefix: `filters[${index}].value`,
3208
+ fixHint: 'Use legacy filter keys "min_value"/"max_value"/"search_key"/"search_keys", or switch to canonical qf.query.* where clauses.'
3209
+ });
3210
+ }
3211
+ }
3212
+ function assertNoTopLevelTimeAliases(obj, tool) {
3213
+ assertNoForbiddenAliases(obj, FORBIDDEN_TOP_LEVEL_TIME_ALIASES, {
3214
+ tool,
3215
+ pathPrefix: "arguments",
3216
+ fixHint: 'Use time_range: {"column": ..., "from": "...", "to": "..."} instead of top-level date aliases.'
3217
+ });
3218
+ }
3219
+ function assertNoValueProbeAliases(obj, tool) {
3220
+ assertNoForbiddenAliases(obj, {
3221
+ searchKey: "query",
3222
+ searchKeys: "query"
3223
+ }, {
3224
+ tool,
3225
+ pathPrefix: "arguments",
3226
+ fixHint: 'Use "query" instead of "searchKey"/"searchKeys".'
3227
+ });
3228
+ }
3020
3229
  function normalizeToolSpecInput(raw) {
3021
3230
  const parsedRoot = parseJsonLikeDeep(raw);
3022
3231
  const obj = asObject(parsedRoot);
@@ -3089,12 +3298,31 @@ function buildToolSpecCatalog() {
3089
3298
  top_k: 3
3090
3299
  }
3091
3300
  },
3301
+ {
3302
+ tool: "qf_value_probe",
3303
+ required: ["app_key", "field"],
3304
+ limits: {
3305
+ limit_max: 50,
3306
+ scan_max_pages_max: 20,
3307
+ page_size_max: 200,
3308
+ match_mode: Array.from(matchModeValues),
3309
+ input_contract: 'strict JSON only; use field plus optional query and match_mode. "searchKey"/"searchKeys" are rejected.'
3310
+ },
3311
+ aliases: {},
3312
+ minimal_example: {
3313
+ app_key: "21b3d559",
3314
+ field: "归属部门",
3315
+ query: "北斗",
3316
+ match_mode: "contains",
3317
+ limit: 10
3318
+ }
3319
+ },
3092
3320
  {
3093
3321
  tool: "qf_query_plan",
3094
3322
  required: ["tool"],
3095
3323
  limits: {
3096
3324
  tool: "qf_records_list|qf_record_get|qf_query|qf_records_aggregate|qf_records_batch_get|qf_export_csv|qf_export_json",
3097
- input_contract: "strict JSON only; arguments must be a native JSON object"
3325
+ input_contract: 'strict JSON only; arguments must be a native JSON object. Target tools reject runtime aliases like from/to/dateFrom/dateTo/searchKey/searchKeys.'
3098
3326
  },
3099
3327
  aliases: {},
3100
3328
  minimal_example: {
@@ -3112,7 +3340,7 @@ function buildToolSpecCatalog() {
3112
3340
  required: ["kind"],
3113
3341
  limits: {
3114
3342
  kind: "rows|record|aggregate|export|mutate",
3115
- input_contract: "plan accepts loose model-shaped query objects and normalizes them before execution"
3343
+ input_contract: "plan accepts loose model-shaped query objects, but rejects runtime aliases like from/to/dateFrom/dateTo/searchKey/searchKeys before execution"
3116
3344
  },
3117
3345
  aliases: {},
3118
3346
  minimal_example: {
@@ -3221,7 +3449,7 @@ function buildToolSpecCatalog() {
3221
3449
  max_columns_max: MAX_COLUMN_LIMIT,
3222
3450
  select_columns_max: MAX_COLUMN_LIMIT,
3223
3451
  output_profile: "compact|verbose (default compact)",
3224
- input_contract: "strict JSON only; numbers/arrays/objects/booleans must use native JSON types"
3452
+ input_contract: "strict JSON only; numbers/arrays/objects/booleans must use native JSON types, and runtime aliases from/to/dateFrom/dateTo/searchKey/searchKeys are rejected"
3225
3453
  },
3226
3454
  aliases: {},
3227
3455
  minimal_example: {
@@ -3276,7 +3504,7 @@ function buildToolSpecCatalog() {
3276
3504
  max_rows_max: EXPORT_MAX_ROWS,
3277
3505
  max_columns_max: MAX_COLUMN_LIMIT,
3278
3506
  select_columns_max: MAX_COLUMN_LIMIT,
3279
- input_contract: "strict JSON only; select_columns/time_range must use native JSON types"
3507
+ input_contract: "strict JSON only; select_columns/time_range must use native JSON types, and runtime aliases from/to/dateFrom/dateTo/searchKey/searchKeys are rejected"
3280
3508
  },
3281
3509
  aliases: {},
3282
3510
  minimal_example: {
@@ -3298,7 +3526,7 @@ function buildToolSpecCatalog() {
3298
3526
  max_rows_max: EXPORT_MAX_ROWS,
3299
3527
  max_columns_max: MAX_COLUMN_LIMIT,
3300
3528
  select_columns_max: MAX_COLUMN_LIMIT,
3301
- input_contract: "strict JSON only; select_columns/time_range must use native JSON types"
3529
+ input_contract: "strict JSON only; select_columns/time_range must use native JSON types, and runtime aliases from/to/dateFrom/dateTo/searchKey/searchKeys are rejected"
3302
3530
  },
3303
3531
  aliases: {},
3304
3532
  minimal_example: {
@@ -3327,7 +3555,7 @@ function buildToolSpecCatalog() {
3327
3555
  max_columns_max: MAX_COLUMN_LIMIT,
3328
3556
  select_columns_max: MAX_COLUMN_LIMIT,
3329
3557
  output_profile: "compact|verbose (default compact)",
3330
- input_contract: "strict JSON only; select_columns/time_range/stat_policy must use native JSON types"
3558
+ input_contract: "strict JSON only; select_columns/time_range/stat_policy must use native JSON types, and runtime aliases from/to/dateFrom/dateTo/searchKey/searchKeys are rejected"
3331
3559
  },
3332
3560
  aliases: {},
3333
3561
  minimal_example: {
@@ -3357,7 +3585,7 @@ function buildToolSpecCatalog() {
3357
3585
  metrics_supported: ["count", "sum", "avg", "min", "max"],
3358
3586
  time_bucket_supported: ["day", "week", "month"],
3359
3587
  output_profile: "compact|verbose (default compact)",
3360
- input_contract: "strict JSON only; group_by/amount_columns/time_range must use native JSON types"
3588
+ input_contract: "strict JSON only; group_by/amount_columns/time_range must use native JSON types, and runtime aliases from/to/dateFrom/dateTo/searchKey/searchKeys are rejected"
3361
3589
  },
3362
3590
  aliases: {},
3363
3591
  minimal_example: {
@@ -3455,6 +3683,8 @@ function normalizeListInput(raw) {
3455
3683
  if (!obj) {
3456
3684
  return parsedRoot;
3457
3685
  }
3686
+ assertNoTopLevelTimeAliases(obj, "qf_records_list");
3687
+ assertNoLegacyFilterAliases(obj.filters, "qf_records_list");
3458
3688
  const normalizedObj = applyAliases(obj, COMMON_INPUT_ALIASES);
3459
3689
  const selectColumns = normalizedObj.select_columns ?? normalizedObj.keep_columns;
3460
3690
  const timeRange = buildFriendlyTimeRangeInput(normalizedObj);
@@ -3500,6 +3730,8 @@ function normalizeQueryInput(raw) {
3500
3730
  if (!obj) {
3501
3731
  return parsedRoot;
3502
3732
  }
3733
+ assertNoTopLevelTimeAliases(obj, "qf_query");
3734
+ assertNoLegacyFilterAliases(obj.filters, "qf_query");
3503
3735
  const normalizedObj = applyAliases(obj, COMMON_INPUT_ALIASES);
3504
3736
  const selectColumns = normalizedObj.select_columns ?? normalizedObj.keep_columns;
3505
3737
  const timeRange = buildFriendlyTimeRangeInput(normalizedObj);
@@ -3532,6 +3764,8 @@ function normalizeAggregateInput(raw) {
3532
3764
  if (!obj) {
3533
3765
  return parsedRoot;
3534
3766
  }
3767
+ assertNoTopLevelTimeAliases(obj, "qf_records_aggregate");
3768
+ assertNoLegacyFilterAliases(obj.filters, "qf_records_aggregate");
3535
3769
  const normalizedObj = applyAliases(obj, COMMON_INPUT_ALIASES);
3536
3770
  const timeRange = buildFriendlyTimeRangeInput(normalizedObj);
3537
3771
  const amountColumns = normalizeAmountColumnsInput(normalizedObj.amount_columns ?? normalizedObj.amount_column);
@@ -3579,6 +3813,38 @@ function normalizeFieldResolveInput(raw) {
3579
3813
  fuzzy: coerceBooleanLike(normalizedObj.fuzzy)
3580
3814
  };
3581
3815
  }
3816
+ function normalizeValueProbeInput(raw) {
3817
+ const parsedRoot = parseJsonLikeDeep(raw);
3818
+ const obj = asObject(parsedRoot);
3819
+ if (!obj) {
3820
+ return parsedRoot;
3821
+ }
3822
+ assertNoValueProbeAliases(obj, "qf_value_probe");
3823
+ const normalizedObj = applyAliases(obj, {
3824
+ appKey: "app_key",
3825
+ fieldId: "field",
3826
+ fieldTitle: "field",
3827
+ name: "field",
3828
+ value: "query",
3829
+ search: "query",
3830
+ prefix: "query",
3831
+ match: "match_mode",
3832
+ matchMode: "match_mode",
3833
+ scanMaxPages: "scan_max_pages",
3834
+ pageSize: "page_size"
3835
+ });
3836
+ return {
3837
+ ...normalizedObj,
3838
+ field: normalizeSelectorInputValue(normalizedObj.field),
3839
+ query: normalizedObj.query !== undefined ? coerceStringLike(normalizedObj.query) : undefined,
3840
+ match_mode: typeof normalizedObj.match_mode === "string" && normalizedObj.match_mode.trim()
3841
+ ? normalizedObj.match_mode.trim().toLowerCase()
3842
+ : undefined,
3843
+ limit: coerceNumberLike(normalizedObj.limit),
3844
+ scan_max_pages: coerceNumberLike(normalizedObj.scan_max_pages),
3845
+ page_size: coerceNumberLike(normalizedObj.page_size)
3846
+ };
3847
+ }
3582
3848
  function normalizeQueryPlanInput(raw) {
3583
3849
  const parsedRoot = parseJsonLikeDeep(raw);
3584
3850
  const obj = asObject(parsedRoot);
@@ -3621,6 +3887,8 @@ function normalizeExportInput(raw) {
3621
3887
  if (!obj) {
3622
3888
  return parsedRoot;
3623
3889
  }
3890
+ assertNoTopLevelTimeAliases(obj, "qf_export");
3891
+ assertNoLegacyFilterAliases(obj.filters, "qf_export");
3624
3892
  const normalizedObj = applyAliases(obj, COMMON_INPUT_ALIASES);
3625
3893
  const selectColumns = normalizedObj.select_columns ?? normalizedObj.keep_columns;
3626
3894
  const timeRange = buildFriendlyTimeRangeInput(normalizedObj);
@@ -3800,8 +4068,6 @@ function normalizeFiltersInput(value) {
3800
4068
  column: "que_id",
3801
4069
  columnId: "que_id",
3802
4070
  columnTitle: "que_title",
3803
- searchKey: "search_key",
3804
- searchKeys: "search_keys",
3805
4071
  minValue: "min_value",
3806
4072
  maxValue: "max_value",
3807
4073
  compareType: "compare_type",
@@ -3810,10 +4076,8 @@ function normalizeFiltersInput(value) {
3810
4076
  users: "search_user_ids",
3811
4077
  options: "search_options",
3812
4078
  start: "min_value",
3813
- from: "min_value",
3814
4079
  min: "min_value",
3815
4080
  end: "max_value",
3816
- to: "max_value",
3817
4081
  max: "max_value"
3818
4082
  });
3819
4083
  const normalizedCompareType = typeof normalizedObj.compare_type === "string"
@@ -3835,17 +4099,11 @@ function normalizeFiltersInput(value) {
3835
4099
  if (valueObject) {
3836
4100
  const valueAliases = applyAliases(valueObject, {
3837
4101
  start: "min_value",
3838
- from: "min_value",
3839
4102
  min: "min_value",
3840
4103
  date_from: "min_value",
3841
- dateFrom: "min_value",
3842
4104
  end: "max_value",
3843
- to: "max_value",
3844
4105
  max: "max_value",
3845
4106
  date_to: "max_value",
3846
- dateTo: "max_value",
3847
- searchKey: "search_key",
3848
- searchKeys: "search_keys",
3849
4107
  searchOptions: "search_options",
3850
4108
  searchUserIds: "search_user_ids"
3851
4109
  });
@@ -4010,8 +4268,8 @@ function buildFriendlyTimeRangeInput(obj) {
4010
4268
  const normalizedRawTimeRange = normalizeTimeRangeInput(obj.time_range);
4011
4269
  const normalizedTimeRange = asObject(normalizedRawTimeRange);
4012
4270
  const columnCandidate = firstPresent(obj.date_field, obj.dateField, obj.time_field, obj.timeField, obj.time_column, obj.timeColumn, obj.date_column, obj.dateColumn, normalizedTimeRange?.column);
4013
- const fromCandidate = firstPresent(obj.date_from, obj.dateFrom, obj.time_from, obj.timeFrom, obj.start, obj.start_date, obj.startDate, obj.from, normalizedTimeRange?.from);
4014
- const toCandidate = firstPresent(obj.date_to, obj.dateTo, obj.time_to, obj.timeTo, obj.end, obj.end_date, obj.endDate, obj.to, normalizedTimeRange?.to);
4271
+ const fromCandidate = firstPresent(obj.date_from, obj.time_from, obj.timeFrom, obj.start, obj.start_date, obj.startDate, normalizedTimeRange?.from);
4272
+ const toCandidate = firstPresent(obj.date_to, obj.time_to, obj.timeTo, obj.end, obj.end_date, obj.endDate, normalizedTimeRange?.to);
4015
4273
  const timezoneCandidate = firstPresent(obj.timezone, obj.timeZone, obj.tz, normalizedTimeRange?.timezone);
4016
4274
  if (columnCandidate === undefined &&
4017
4275
  fromCandidate === undefined &&
@@ -4280,7 +4538,14 @@ function buildExtendedCompleteness(params) {
4280
4538
  output_page_complete: outputPageComplete,
4281
4539
  raw_next_page_token: params.rawNextPageToken ?? params.nextPageToken,
4282
4540
  output_next_page_token: params.outputNextPageToken ?? null,
4283
- stop_reason: params.stopReason ?? null
4541
+ stop_reason: params.stopReason ?? null,
4542
+ provider_result_amount: params.providerResultAmount ?? params.resultAmount,
4543
+ source_record_count: params.sourceRecordCount ?? params.returnedItems,
4544
+ effective_record_count: params.effectiveRecordCount ?? params.returnedItems,
4545
+ ...(params.returnedGroupCount !== undefined
4546
+ ? { returned_group_count: params.returnedGroupCount }
4547
+ : {}),
4548
+ ...(params.totalGroupCount !== undefined ? { total_group_count: params.totalGroupCount } : {})
4284
4549
  };
4285
4550
  }
4286
4551
  function normalizePlanToolName(tool) {
@@ -5070,6 +5335,10 @@ function buildRecordGetArgsFromQuery(args) {
5070
5335
  }
5071
5336
  function normalizeCanonicalLooseInput(kind, raw) {
5072
5337
  const parsed = asObject(parseJsonLikeDeep(raw)) ?? {};
5338
+ if (kind === "rows" || kind === "aggregate" || kind === "export") {
5339
+ assertNoTopLevelTimeAliases(parsed, "qf.query.plan");
5340
+ assertNoLegacyFilterAliases(parsed.filters, "qf.query.plan");
5341
+ }
5073
5342
  const out = { ...parsed };
5074
5343
  const assignAlias = (target, aliases) => {
5075
5344
  if (out[target] !== undefined) {
@@ -5208,6 +5477,39 @@ function normalizeCanonicalLooseInput(kind, raw) {
5208
5477
  };
5209
5478
  });
5210
5479
  }
5480
+ return pickCanonicalLooseFields(kind, out);
5481
+ }
5482
+ function pickCanonicalLooseFields(kind, value) {
5483
+ const commonKeys = [
5484
+ "app_key",
5485
+ "select",
5486
+ "where",
5487
+ "sort",
5488
+ "limit",
5489
+ "cursor",
5490
+ "page_size",
5491
+ "requested_pages",
5492
+ "scan_max_pages",
5493
+ "strict_full",
5494
+ "output_profile",
5495
+ "user_id",
5496
+ "mode"
5497
+ ];
5498
+ const allowedKeys = kind === "rows"
5499
+ ? commonKeys
5500
+ : kind === "record"
5501
+ ? ["apply_id", "select", "output_profile"]
5502
+ : kind === "aggregate"
5503
+ ? [...commonKeys.filter((item) => item !== "select"), "group_by", "metrics", "time_bucket", "top_n"]
5504
+ : kind === "export"
5505
+ ? [...commonKeys, "format", "file_name", "export_dir"]
5506
+ : ["action", "app_key", "apply_id", "user_id", "fields", "answers", "force_refresh_form"];
5507
+ const out = {};
5508
+ for (const key of allowedKeys) {
5509
+ if (value[key] !== undefined) {
5510
+ out[key] = value[key];
5511
+ }
5512
+ }
5211
5513
  return out;
5212
5514
  }
5213
5515
  function legacyFiltersToCanonicalWhere(filters, timeRange) {
@@ -5414,7 +5716,7 @@ async function buildCanonicalAggregateExecution(input) {
5414
5716
  filters: translation.filters,
5415
5717
  sort: translateCanonicalSortToLegacy(input.sort),
5416
5718
  group_by: input.group_by,
5417
- amount_columns: amountColumns,
5719
+ ...(amountColumns.length > 0 ? { amount_columns: amountColumns } : {}),
5418
5720
  metrics: metricNames,
5419
5721
  time_bucket: input.time_bucket,
5420
5722
  max_groups: input.top_n,
@@ -5475,7 +5777,9 @@ async function buildCanonicalExportExecution(input) {
5475
5777
  scan_max_pages: input.scan_max_pages ?? (input.requested_pages ?? EXPORT_DEFAULT_PAGES),
5476
5778
  strict_full: input.strict_full ?? false,
5477
5779
  mode: input.mode ?? "all",
5478
- format
5780
+ format,
5781
+ ...(input.file_name ? { file_name: input.file_name } : {}),
5782
+ ...(input.export_dir ? { export_dir: input.export_dir } : {})
5479
5783
  },
5480
5784
  internalTool: format === "csv" ? "qf_export_csv" : "qf_export_json",
5481
5785
  internalArguments,
@@ -5495,7 +5799,10 @@ async function buildCanonicalMutateExecution(input) {
5495
5799
  normalizedQuery: {
5496
5800
  action: input.action,
5497
5801
  app_key: input.app_key ?? null,
5498
- fields: input.fields ?? null
5802
+ user_id: input.user_id ?? null,
5803
+ fields: input.fields ?? null,
5804
+ answers: input.answers ?? null,
5805
+ force_refresh_form: input.force_refresh_form ?? false
5499
5806
  },
5500
5807
  internalTool: "qf_record_create",
5501
5808
  internalArguments
@@ -5514,12 +5821,30 @@ async function buildCanonicalMutateExecution(input) {
5514
5821
  action: input.action,
5515
5822
  app_key: input.app_key ?? null,
5516
5823
  apply_id: input.apply_id ?? null,
5517
- fields: input.fields ?? null
5824
+ user_id: input.user_id ?? null,
5825
+ fields: input.fields ?? null,
5826
+ answers: input.answers ?? null,
5827
+ force_refresh_form: input.force_refresh_form ?? false
5518
5828
  },
5519
5829
  internalTool: "qf_record_update",
5520
5830
  internalArguments
5521
5831
  };
5522
5832
  }
5833
+ function canonicalExecutorToolForKind(kind) {
5834
+ if (kind === "rows") {
5835
+ return "qf.query.rows";
5836
+ }
5837
+ if (kind === "record") {
5838
+ return "qf.query.record";
5839
+ }
5840
+ if (kind === "aggregate") {
5841
+ return "qf.query.aggregate";
5842
+ }
5843
+ if (kind === "export") {
5844
+ return "qf.query.export";
5845
+ }
5846
+ return "qf.records.mutate";
5847
+ }
5523
5848
  async function executeFieldResolve(args) {
5524
5849
  const response = await getFormCached(args.app_key, undefined, false);
5525
5850
  const index = buildFieldIndex(response.result);
@@ -5544,6 +5869,42 @@ async function executeFieldResolve(args) {
5544
5869
  meta: buildMeta(response)
5545
5870
  };
5546
5871
  }
5872
+ async function executeValueProbe(args) {
5873
+ const details = await collectFieldValueCandidatesDetailed({
5874
+ appKey: args.app_key,
5875
+ field: String(args.field),
5876
+ prefix: args.query,
5877
+ match: args.match_mode,
5878
+ limit: args.limit,
5879
+ scanMaxPages: args.scan_max_pages,
5880
+ pageSize: args.page_size
5881
+ });
5882
+ return {
5883
+ ok: true,
5884
+ data: {
5885
+ app_key: args.app_key,
5886
+ field: {
5887
+ requested: details.field.requested,
5888
+ que_id: details.field.que_id,
5889
+ que_title: details.field.que_title,
5890
+ que_type: details.field.que_type
5891
+ },
5892
+ query: details.query || null,
5893
+ requested_match_mode: details.requested_match_mode,
5894
+ effective_match_mode: details.effective_match_mode,
5895
+ provider_translation: details.provider_translation,
5896
+ scanned_pages: details.scanned_pages,
5897
+ scanned_records: details.scanned_records,
5898
+ provider_result_amount: details.provider_result_amount,
5899
+ candidates: details.candidates,
5900
+ matched_values: details.candidates.map((item) => item.value)
5901
+ },
5902
+ meta: {
5903
+ version: SERVER_VERSION,
5904
+ generated_at: new Date().toISOString()
5905
+ }
5906
+ };
5907
+ }
5547
5908
  async function executeQueryPlan(args) {
5548
5909
  const normalizedTool = normalizePlanToolName(args.tool);
5549
5910
  const rawArguments = asObject(parseJsonLikeDeep(args.arguments)) ?? {};
@@ -5648,7 +6009,10 @@ async function executeRecordsBatchGet(args) {
5648
6009
  is_complete: missingApplyIds.length === 0,
5649
6010
  partial: missingApplyIds.length > 0,
5650
6011
  omitted_items: missingApplyIds.length,
5651
- omitted_chars: 0
6012
+ omitted_chars: 0,
6013
+ provider_result_amount: requestedApplyIds.length,
6014
+ source_record_count: rows.length,
6015
+ effective_record_count: rows.length
5652
6016
  };
5653
6017
  const evidence = buildEvidencePayload({
5654
6018
  query_id: queryId,
@@ -5862,7 +6226,10 @@ async function executeRecordsExport(format, args) {
5862
6226
  is_complete: !hasMore && omittedItems === 0,
5863
6227
  partial: hasMore || omittedItems > 0,
5864
6228
  omitted_items: omittedItems,
5865
- omitted_chars: 0
6229
+ omitted_chars: 0,
6230
+ provider_result_amount: knownResultAmount,
6231
+ source_record_count: rows.length,
6232
+ effective_record_count: rows.length
5866
6233
  };
5867
6234
  const evidence = buildEvidencePayload({
5868
6235
  query_id: queryId,
@@ -6085,7 +6452,10 @@ async function executeRecordsList(args) {
6085
6452
  is_complete: isComplete,
6086
6453
  partial: !isComplete,
6087
6454
  omitted_items: omittedItems,
6088
- omitted_chars: fittedRows.omittedChars
6455
+ omitted_chars: fittedRows.omittedChars,
6456
+ provider_result_amount: knownResultAmount,
6457
+ source_record_count: collectedRawItems.length,
6458
+ effective_record_count: fittedRows.items.length
6089
6459
  };
6090
6460
  const listState = {
6091
6461
  query_id: queryId,
@@ -6197,7 +6567,10 @@ async function executeRecordGet(args) {
6197
6567
  is_complete: true,
6198
6568
  partial: false,
6199
6569
  omitted_items: 0,
6200
- omitted_chars: 0
6570
+ omitted_chars: 0,
6571
+ provider_result_amount: 1,
6572
+ source_record_count: 1,
6573
+ effective_record_count: 1
6201
6574
  };
6202
6575
  const evidence = {
6203
6576
  query_id: queryId,
@@ -6622,7 +6995,10 @@ async function executeRecordsSummary(args) {
6622
6995
  outputPageComplete,
6623
6996
  rawNextPageToken,
6624
6997
  outputNextPageToken: null,
6625
- stopReason
6998
+ stopReason,
6999
+ providerResultAmount: knownResultAmount,
7000
+ sourceRecordCount: scannedRecords,
7001
+ effectiveRecordCount: scannedRecords
6626
7002
  });
6627
7003
  const evidence = buildEvidencePayload(listState, sourcePages);
6628
7004
  if (strictFull && !rawScanComplete) {
@@ -6924,7 +7300,12 @@ async function executeRecordsAggregate(args) {
6924
7300
  outputPageComplete,
6925
7301
  rawNextPageToken,
6926
7302
  outputNextPageToken: null,
6927
- stopReason
7303
+ stopReason,
7304
+ providerResultAmount: knownResultAmount,
7305
+ sourceRecordCount: scannedRecords,
7306
+ effectiveRecordCount: scannedRecords,
7307
+ returnedGroupCount: Math.min(groupsTotal, maxGroups),
7308
+ totalGroupCount: groupsTotal
6928
7309
  });
6929
7310
  const evidence = buildEvidencePayload(listState, sourcePages);
6930
7311
  if (strictFull && !completeness.is_complete) {
@@ -7528,38 +7909,77 @@ async function completeFieldCandidates(appKey, prefix) {
7528
7909
  .slice(0, 20);
7529
7910
  }
7530
7911
  async function collectFieldValueCandidates(params) {
7912
+ const details = await collectFieldValueCandidatesDetailed(params);
7913
+ return details.candidates;
7914
+ }
7915
+ async function collectFieldValueCandidatesDetailed(params) {
7531
7916
  const appKey = params.appKey.trim();
7532
7917
  const fieldSelector = params.field.trim();
7533
7918
  if (!appKey || !fieldSelector) {
7534
- return [];
7919
+ throw new InputValidationError({
7920
+ message: "app_key and field are required for qf_value_probe",
7921
+ errorCode: "MISSING_REQUIRED_FIELD",
7922
+ fixHint: "Pass app_key plus field (que_id or field title).",
7923
+ details: {
7924
+ app_key: appKey || null,
7925
+ field: fieldSelector || null
7926
+ }
7927
+ });
7535
7928
  }
7536
7929
  const form = await getFormCached(appKey, undefined, false);
7537
7930
  const index = buildFieldIndex(form.result);
7538
7931
  const column = resolveSummaryColumn(fieldSelector, index, "field");
7539
7932
  const counts = new Map();
7540
7933
  let currentPage = 1;
7541
- for (let fetched = 0; fetched < 5; fetched += 1) {
7934
+ let scannedPages = 0;
7935
+ let scannedRecords = 0;
7936
+ let providerResultAmount = null;
7937
+ const query = (params.prefix ?? "").trim();
7938
+ const requestedMatchMode = normalizeMatchMode(params.match);
7939
+ const limit = params.limit ?? 20;
7940
+ const scanMaxPages = params.scanMaxPages ?? 5;
7941
+ const pageSize = params.pageSize ?? 50;
7942
+ for (let fetched = 0; fetched < scanMaxPages; fetched += 1) {
7542
7943
  const payload = buildListPayload({
7543
7944
  pageNum: currentPage,
7544
- pageSize: 50,
7945
+ pageSize,
7545
7946
  mode: "all"
7546
7947
  });
7547
7948
  const response = await client.listRecords(appKey, payload, {});
7548
7949
  const result = asObject(response.result);
7549
7950
  const rawItems = asArray(result?.result);
7951
+ providerResultAmount = providerResultAmount ?? toNonNegativeInt(result?.resultAmount);
7952
+ scannedPages += 1;
7953
+ scannedRecords += rawItems.length;
7550
7954
  for (const rawItem of rawItems) {
7551
7955
  const record = asObject(rawItem) ?? {};
7552
7956
  const value = extractSummaryColumnValue(asArray(record.answers), column);
7553
- const candidates = Array.isArray(value) ? value : [value];
7554
- for (const candidate of candidates) {
7555
- if (candidate === null || candidate === undefined) {
7556
- continue;
7557
- }
7558
- const normalized = String(candidate).trim();
7559
- if (!normalized) {
7560
- continue;
7957
+ if (value === null || value === undefined) {
7958
+ continue;
7959
+ }
7960
+ const normalized = normalizeProbeValue(value);
7961
+ if (!normalized.display_value) {
7962
+ continue;
7963
+ }
7964
+ const matchDetails = scoreProbeValueMatch(value, query, requestedMatchMode);
7965
+ const existing = counts.get(normalized.key);
7966
+ if (existing) {
7967
+ existing.count += 1;
7968
+ if (matchDetails.match_strength > existing.match_strength) {
7969
+ existing.match_strength = matchDetails.match_strength;
7970
+ existing.matched_texts = matchDetails.matched_texts;
7971
+ existing.matched_as = matchDetails.matched_as;
7561
7972
  }
7562
- counts.set(normalized, (counts.get(normalized) ?? 0) + 1);
7973
+ }
7974
+ else {
7975
+ counts.set(normalized.key, {
7976
+ value: normalized.value,
7977
+ display_value: normalized.display_value,
7978
+ count: 1,
7979
+ match_strength: matchDetails.match_strength,
7980
+ matched_texts: matchDetails.matched_texts,
7981
+ matched_as: matchDetails.matched_as
7982
+ });
7563
7983
  }
7564
7984
  }
7565
7985
  const pageAmount = toPositiveInt(result?.pageAmount);
@@ -7568,20 +7988,21 @@ async function collectFieldValueCandidates(params) {
7568
7988
  }
7569
7989
  currentPage += 1;
7570
7990
  }
7571
- const normalizedPrefix = (params.prefix ?? "").trim().toLowerCase();
7572
- const match = (params.match ?? "contains").trim().toLowerCase();
7573
- const limit = params.limit ?? 20;
7574
- return Array.from(counts.entries())
7575
- .map(([value, count]) => ({
7576
- value,
7577
- count,
7578
- match_strength: normalizedPrefix.length === 0
7579
- ? 1
7580
- : scoreTextMatch(value, normalizedPrefix, match)
7581
- }))
7991
+ const candidates = Array.from(counts.values())
7582
7992
  .filter((item) => item.match_strength > 0)
7583
7993
  .sort((a, b) => b.match_strength - a.match_strength || b.count - a.count)
7584
7994
  .slice(0, limit);
7995
+ return {
7996
+ field: column,
7997
+ query,
7998
+ requested_match_mode: requestedMatchMode,
7999
+ effective_match_mode: requestedMatchMode,
8000
+ provider_translation: "local_distinct_scan",
8001
+ scanned_pages: scannedPages,
8002
+ scanned_records: scannedRecords,
8003
+ provider_result_amount: providerResultAmount,
8004
+ candidates
8005
+ };
7585
8006
  }
7586
8007
  function scoreTextMatch(candidate, prefix, match) {
7587
8008
  const normalizedCandidate = candidate.trim().toLowerCase();
@@ -7602,6 +8023,84 @@ function scoreTextMatch(candidate, prefix, match) {
7602
8023
  }
7603
8024
  return normalizedCandidate.includes(prefix) ? 0.9 : 0;
7604
8025
  }
8026
+ function normalizeMatchMode(value) {
8027
+ if (typeof value === "string") {
8028
+ const normalized = value.trim().toLowerCase();
8029
+ if (matchModeValues.includes(normalized)) {
8030
+ return normalized;
8031
+ }
8032
+ }
8033
+ return "contains";
8034
+ }
8035
+ function normalizeProbeValue(value) {
8036
+ if (value === null || value === undefined) {
8037
+ return {
8038
+ key: "null",
8039
+ value,
8040
+ display_value: ""
8041
+ };
8042
+ }
8043
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
8044
+ return {
8045
+ key: stableJson(value),
8046
+ value,
8047
+ display_value: String(value).trim()
8048
+ };
8049
+ }
8050
+ return {
8051
+ key: stableJson(value),
8052
+ value,
8053
+ display_value: stableJson(value)
8054
+ };
8055
+ }
8056
+ function scoreProbeValueMatch(value, query, matchMode) {
8057
+ if (!query) {
8058
+ return {
8059
+ match_strength: 1,
8060
+ matched_texts: [],
8061
+ matched_as: "all"
8062
+ };
8063
+ }
8064
+ const normalizedQuery = query.trim().toLowerCase();
8065
+ const normalized = normalizeProbeValue(value);
8066
+ let bestScore = scoreTextMatch(normalized.display_value.toLowerCase(), normalizedQuery, matchMode);
8067
+ let matchedTexts = bestScore > 0 ? [normalized.display_value] : [];
8068
+ let matchedAs = bestScore > 0 ? matchMode : "none";
8069
+ const leafTexts = flattenProbeLeafTexts(value);
8070
+ for (const leaf of leafTexts) {
8071
+ const score = scoreTextMatch(leaf.toLowerCase(), normalizedQuery, matchMode);
8072
+ if (score > bestScore) {
8073
+ bestScore = score;
8074
+ matchedTexts = [leaf];
8075
+ matchedAs = Array.isArray(value)
8076
+ ? `array_${matchMode}`
8077
+ : value && typeof value === "object"
8078
+ ? `object_${matchMode}`
8079
+ : matchMode;
8080
+ }
8081
+ }
8082
+ return {
8083
+ match_strength: Number(Math.min(1, Math.max(0, bestScore)).toFixed(4)),
8084
+ matched_texts: uniqueStringList(matchedTexts),
8085
+ matched_as: matchedAs
8086
+ };
8087
+ }
8088
+ function flattenProbeLeafTexts(value, depth = 0) {
8089
+ if (depth > 4 || value === null || value === undefined) {
8090
+ return [];
8091
+ }
8092
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
8093
+ const text = String(value).trim();
8094
+ return text ? [text] : [];
8095
+ }
8096
+ if (Array.isArray(value)) {
8097
+ return uniqueStringList(value.flatMap((item) => flattenProbeLeafTexts(item, depth + 1)));
8098
+ }
8099
+ if (typeof value === "object") {
8100
+ return uniqueStringList(Object.values(value).flatMap((item) => flattenProbeLeafTexts(item, depth + 1)));
8101
+ }
8102
+ return [];
8103
+ }
7605
8104
  async function completeFieldValueCandidates(appKey, field, prefix, match) {
7606
8105
  const candidates = await collectFieldValueCandidates({
7607
8106
  appKey,
@@ -7610,7 +8109,7 @@ async function completeFieldValueCandidates(appKey, field, prefix, match) {
7610
8109
  match,
7611
8110
  limit: 10
7612
8111
  });
7613
- return candidates.map((item) => item.value);
8112
+ return candidates.map((item) => item.display_value);
7614
8113
  }
7615
8114
  function completeQueryIdCandidates(prefix) {
7616
8115
  cleanupArtifactCaches();
@@ -7657,17 +8156,29 @@ async function readFieldValuesResource(uri, variables) {
7657
8156
  if (!appKey || !field) {
7658
8157
  throw new Error("app_key and field are required");
7659
8158
  }
7660
- return jsonResourceResponse(uri, {
7661
- app_key: appKey,
8159
+ const details = await collectFieldValueCandidatesDetailed({
8160
+ appKey,
7662
8161
  field,
7663
- match,
7664
8162
  prefix,
7665
- candidates: await collectFieldValueCandidates({
7666
- appKey,
7667
- field,
7668
- prefix,
7669
- match
7670
- })
8163
+ match
8164
+ });
8165
+ return jsonResourceResponse(uri, {
8166
+ app_key: appKey,
8167
+ field: {
8168
+ requested: details.field.requested,
8169
+ que_id: details.field.que_id,
8170
+ que_title: details.field.que_title,
8171
+ que_type: details.field.que_type
8172
+ },
8173
+ query: details.query || null,
8174
+ requested_match_mode: details.requested_match_mode,
8175
+ effective_match_mode: details.effective_match_mode,
8176
+ provider_translation: details.provider_translation,
8177
+ scanned_pages: details.scanned_pages,
8178
+ scanned_records: details.scanned_records,
8179
+ provider_result_amount: details.provider_result_amount,
8180
+ candidates: details.candidates,
8181
+ matched_values: details.candidates.map((item) => item.value)
7671
8182
  });
7672
8183
  }
7673
8184
  async function readQueryNormalizedResource(uri, variables) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qingflow-mcp",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "private": false,
5
5
  "license": "MIT",
6
6
  "type": "module",