qingflow-mcp 0.4.1 → 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.
Files changed (3) hide show
  1. package/README.md +2 -2
  2. package/dist/server.js +118 -30
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -109,7 +109,7 @@ npm i -g git+https://github.com/853046310/qingflow-mcp.git
109
109
  Install from npm (pinned version):
110
110
 
111
111
  ```bash
112
- npm i -g qingflow-mcp@0.4.1
112
+ npm i -g qingflow-mcp@0.4.2
113
113
  ```
114
114
 
115
115
  Or one-click installer:
@@ -217,7 +217,7 @@ Deterministic read protocol (list/summary/aggregate):
217
217
  - objects must be native JSON objects
218
218
  - booleans must be native JSON booleans
219
219
  - unknown fields are rejected by the MCP boundary
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.
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.
221
221
 
222
222
  For `qf_query(summary)` and `qf_records_aggregate`, read `data.summary.completeness` / `data.completeness` before concluding:
223
223
 
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.1";
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) {
@@ -244,7 +244,7 @@ const publicFieldSelectorSchema = z.union([publicStringSchema, z.number().int()]
244
244
  const publicSortItemSchema = z.object({
245
245
  que_id: publicFieldSelectorSchema,
246
246
  ascend: z.boolean().optional()
247
- });
247
+ }).strict();
248
248
  const publicFilterItemSchema = z.object({
249
249
  que_id: publicFieldSelectorSchema.optional(),
250
250
  search_key: publicStringSchema.optional(),
@@ -254,17 +254,17 @@ const publicFilterItemSchema = z.object({
254
254
  scope: z.number().int().optional(),
255
255
  search_options: z.array(publicFieldSelectorSchema).optional(),
256
256
  search_user_ids: z.array(publicStringSchema).optional()
257
- });
257
+ }).strict();
258
258
  const publicTimeRangeSchema = z.object({
259
259
  column: publicFieldSelectorSchema,
260
260
  from: publicStringSchema.optional(),
261
261
  to: publicStringSchema.optional(),
262
262
  timezone: publicStringSchema.optional()
263
- });
263
+ }).strict();
264
264
  const publicStatPolicySchema = z.object({
265
265
  include_negative: z.boolean().optional(),
266
266
  include_null: z.boolean().optional()
267
- });
267
+ }).strict();
268
268
  const publicAnswerInputSchema = z.object({
269
269
  que_id: publicFieldSelectorSchema.optional(),
270
270
  queId: publicFieldSelectorSchema.optional(),
@@ -349,7 +349,8 @@ const listInputPublicSchema = z
349
349
  include_answers: z.boolean().optional(),
350
350
  strict_full: z.boolean().optional(),
351
351
  output_profile: outputProfileSchema.optional()
352
- });
352
+ })
353
+ .strict();
353
354
  const listInputSchema = z
354
355
  .preprocess(normalizeListInput, z.object({
355
356
  app_key: z.string().min(1).optional(),
@@ -610,7 +611,8 @@ const queryInputPublicSchema = z
610
611
  scan_max_pages: z.number().int().positive().max(500).optional(),
611
612
  strict_full: z.boolean().optional(),
612
613
  output_profile: outputProfileSchema.optional()
613
- });
614
+ })
615
+ .strict();
614
616
  const queryInputSchema = z
615
617
  .preprocess(normalizeQueryInput, z.object({
616
618
  query_mode: z.enum(["auto", "list", "record", "summary"]).optional(),
@@ -792,7 +794,8 @@ const aggregateInputPublicSchema = z
792
794
  max_groups: z.number().int().positive().max(2000).optional(),
793
795
  strict_full: z.boolean().optional(),
794
796
  output_profile: outputProfileSchema.optional()
795
- });
797
+ })
798
+ .strict();
796
799
  const aggregateInputSchema = z
797
800
  .preprocess(normalizeAggregateInput, z.object({
798
801
  app_key: z.string().min(1),
@@ -1133,7 +1136,8 @@ const exportInputPublicSchema = z
1133
1136
  output_profile: outputProfileSchema.optional(),
1134
1137
  export_dir: publicStringSchema.optional(),
1135
1138
  file_name: publicStringSchema.optional()
1136
- });
1139
+ })
1140
+ .strict();
1137
1141
  const exportInputSchema = z.preprocess(normalizeExportInput, z.object({
1138
1142
  app_key: z.string().min(1).optional(),
1139
1143
  user_id: z.string().min(1).optional(),
@@ -3140,6 +3144,88 @@ function applyAliases(obj, aliases) {
3140
3144
  }
3141
3145
  return out;
3142
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
+ }
3143
3229
  function normalizeToolSpecInput(raw) {
3144
3230
  const parsedRoot = parseJsonLikeDeep(raw);
3145
3231
  const obj = asObject(parsedRoot);
@@ -3220,7 +3306,7 @@ function buildToolSpecCatalog() {
3220
3306
  scan_max_pages_max: 20,
3221
3307
  page_size_max: 200,
3222
3308
  match_mode: Array.from(matchModeValues),
3223
- input_contract: "strict JSON only; use field plus optional query and match_mode"
3309
+ input_contract: 'strict JSON only; use field plus optional query and match_mode. "searchKey"/"searchKeys" are rejected.'
3224
3310
  },
3225
3311
  aliases: {},
3226
3312
  minimal_example: {
@@ -3236,7 +3322,7 @@ function buildToolSpecCatalog() {
3236
3322
  required: ["tool"],
3237
3323
  limits: {
3238
3324
  tool: "qf_records_list|qf_record_get|qf_query|qf_records_aggregate|qf_records_batch_get|qf_export_csv|qf_export_json",
3239
- 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.'
3240
3326
  },
3241
3327
  aliases: {},
3242
3328
  minimal_example: {
@@ -3254,7 +3340,7 @@ function buildToolSpecCatalog() {
3254
3340
  required: ["kind"],
3255
3341
  limits: {
3256
3342
  kind: "rows|record|aggregate|export|mutate",
3257
- 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"
3258
3344
  },
3259
3345
  aliases: {},
3260
3346
  minimal_example: {
@@ -3363,7 +3449,7 @@ function buildToolSpecCatalog() {
3363
3449
  max_columns_max: MAX_COLUMN_LIMIT,
3364
3450
  select_columns_max: MAX_COLUMN_LIMIT,
3365
3451
  output_profile: "compact|verbose (default compact)",
3366
- 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"
3367
3453
  },
3368
3454
  aliases: {},
3369
3455
  minimal_example: {
@@ -3418,7 +3504,7 @@ function buildToolSpecCatalog() {
3418
3504
  max_rows_max: EXPORT_MAX_ROWS,
3419
3505
  max_columns_max: MAX_COLUMN_LIMIT,
3420
3506
  select_columns_max: MAX_COLUMN_LIMIT,
3421
- 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"
3422
3508
  },
3423
3509
  aliases: {},
3424
3510
  minimal_example: {
@@ -3440,7 +3526,7 @@ function buildToolSpecCatalog() {
3440
3526
  max_rows_max: EXPORT_MAX_ROWS,
3441
3527
  max_columns_max: MAX_COLUMN_LIMIT,
3442
3528
  select_columns_max: MAX_COLUMN_LIMIT,
3443
- 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"
3444
3530
  },
3445
3531
  aliases: {},
3446
3532
  minimal_example: {
@@ -3469,7 +3555,7 @@ function buildToolSpecCatalog() {
3469
3555
  max_columns_max: MAX_COLUMN_LIMIT,
3470
3556
  select_columns_max: MAX_COLUMN_LIMIT,
3471
3557
  output_profile: "compact|verbose (default compact)",
3472
- 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"
3473
3559
  },
3474
3560
  aliases: {},
3475
3561
  minimal_example: {
@@ -3499,7 +3585,7 @@ function buildToolSpecCatalog() {
3499
3585
  metrics_supported: ["count", "sum", "avg", "min", "max"],
3500
3586
  time_bucket_supported: ["day", "week", "month"],
3501
3587
  output_profile: "compact|verbose (default compact)",
3502
- 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"
3503
3589
  },
3504
3590
  aliases: {},
3505
3591
  minimal_example: {
@@ -3597,6 +3683,8 @@ function normalizeListInput(raw) {
3597
3683
  if (!obj) {
3598
3684
  return parsedRoot;
3599
3685
  }
3686
+ assertNoTopLevelTimeAliases(obj, "qf_records_list");
3687
+ assertNoLegacyFilterAliases(obj.filters, "qf_records_list");
3600
3688
  const normalizedObj = applyAliases(obj, COMMON_INPUT_ALIASES);
3601
3689
  const selectColumns = normalizedObj.select_columns ?? normalizedObj.keep_columns;
3602
3690
  const timeRange = buildFriendlyTimeRangeInput(normalizedObj);
@@ -3642,6 +3730,8 @@ function normalizeQueryInput(raw) {
3642
3730
  if (!obj) {
3643
3731
  return parsedRoot;
3644
3732
  }
3733
+ assertNoTopLevelTimeAliases(obj, "qf_query");
3734
+ assertNoLegacyFilterAliases(obj.filters, "qf_query");
3645
3735
  const normalizedObj = applyAliases(obj, COMMON_INPUT_ALIASES);
3646
3736
  const selectColumns = normalizedObj.select_columns ?? normalizedObj.keep_columns;
3647
3737
  const timeRange = buildFriendlyTimeRangeInput(normalizedObj);
@@ -3674,6 +3764,8 @@ function normalizeAggregateInput(raw) {
3674
3764
  if (!obj) {
3675
3765
  return parsedRoot;
3676
3766
  }
3767
+ assertNoTopLevelTimeAliases(obj, "qf_records_aggregate");
3768
+ assertNoLegacyFilterAliases(obj.filters, "qf_records_aggregate");
3677
3769
  const normalizedObj = applyAliases(obj, COMMON_INPUT_ALIASES);
3678
3770
  const timeRange = buildFriendlyTimeRangeInput(normalizedObj);
3679
3771
  const amountColumns = normalizeAmountColumnsInput(normalizedObj.amount_columns ?? normalizedObj.amount_column);
@@ -3727,6 +3819,7 @@ function normalizeValueProbeInput(raw) {
3727
3819
  if (!obj) {
3728
3820
  return parsedRoot;
3729
3821
  }
3822
+ assertNoValueProbeAliases(obj, "qf_value_probe");
3730
3823
  const normalizedObj = applyAliases(obj, {
3731
3824
  appKey: "app_key",
3732
3825
  fieldId: "field",
@@ -3734,7 +3827,6 @@ function normalizeValueProbeInput(raw) {
3734
3827
  name: "field",
3735
3828
  value: "query",
3736
3829
  search: "query",
3737
- searchKey: "query",
3738
3830
  prefix: "query",
3739
3831
  match: "match_mode",
3740
3832
  matchMode: "match_mode",
@@ -3795,6 +3887,8 @@ function normalizeExportInput(raw) {
3795
3887
  if (!obj) {
3796
3888
  return parsedRoot;
3797
3889
  }
3890
+ assertNoTopLevelTimeAliases(obj, "qf_export");
3891
+ assertNoLegacyFilterAliases(obj.filters, "qf_export");
3798
3892
  const normalizedObj = applyAliases(obj, COMMON_INPUT_ALIASES);
3799
3893
  const selectColumns = normalizedObj.select_columns ?? normalizedObj.keep_columns;
3800
3894
  const timeRange = buildFriendlyTimeRangeInput(normalizedObj);
@@ -3974,8 +4068,6 @@ function normalizeFiltersInput(value) {
3974
4068
  column: "que_id",
3975
4069
  columnId: "que_id",
3976
4070
  columnTitle: "que_title",
3977
- searchKey: "search_key",
3978
- searchKeys: "search_keys",
3979
4071
  minValue: "min_value",
3980
4072
  maxValue: "max_value",
3981
4073
  compareType: "compare_type",
@@ -3984,10 +4076,8 @@ function normalizeFiltersInput(value) {
3984
4076
  users: "search_user_ids",
3985
4077
  options: "search_options",
3986
4078
  start: "min_value",
3987
- from: "min_value",
3988
4079
  min: "min_value",
3989
4080
  end: "max_value",
3990
- to: "max_value",
3991
4081
  max: "max_value"
3992
4082
  });
3993
4083
  const normalizedCompareType = typeof normalizedObj.compare_type === "string"
@@ -4009,17 +4099,11 @@ function normalizeFiltersInput(value) {
4009
4099
  if (valueObject) {
4010
4100
  const valueAliases = applyAliases(valueObject, {
4011
4101
  start: "min_value",
4012
- from: "min_value",
4013
4102
  min: "min_value",
4014
4103
  date_from: "min_value",
4015
- dateFrom: "min_value",
4016
4104
  end: "max_value",
4017
- to: "max_value",
4018
4105
  max: "max_value",
4019
4106
  date_to: "max_value",
4020
- dateTo: "max_value",
4021
- searchKey: "search_key",
4022
- searchKeys: "search_keys",
4023
4107
  searchOptions: "search_options",
4024
4108
  searchUserIds: "search_user_ids"
4025
4109
  });
@@ -4184,8 +4268,8 @@ function buildFriendlyTimeRangeInput(obj) {
4184
4268
  const normalizedRawTimeRange = normalizeTimeRangeInput(obj.time_range);
4185
4269
  const normalizedTimeRange = asObject(normalizedRawTimeRange);
4186
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);
4187
- const fromCandidate = firstPresent(obj.date_from, obj.dateFrom, obj.time_from, obj.timeFrom, obj.start, obj.start_date, obj.startDate, obj.from, normalizedTimeRange?.from);
4188
- 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);
4189
4273
  const timezoneCandidate = firstPresent(obj.timezone, obj.timeZone, obj.tz, normalizedTimeRange?.timezone);
4190
4274
  if (columnCandidate === undefined &&
4191
4275
  fromCandidate === undefined &&
@@ -5251,6 +5335,10 @@ function buildRecordGetArgsFromQuery(args) {
5251
5335
  }
5252
5336
  function normalizeCanonicalLooseInput(kind, raw) {
5253
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
+ }
5254
5342
  const out = { ...parsed };
5255
5343
  const assignAlias = (target, aliases) => {
5256
5344
  if (out[target] !== undefined) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qingflow-mcp",
3
- "version": "0.4.1",
3
+ "version": "0.4.2",
4
4
  "private": false,
5
5
  "license": "MIT",
6
6
  "type": "module",