qingflow-mcp 0.3.19 → 0.3.21

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 +1 -1
  2. package/dist/server.js +155 -24
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -117,7 +117,7 @@ npm i -g git+https://github.com/853046310/qingflow-mcp.git
117
117
  Install from npm (pinned version):
118
118
 
119
119
  ```bash
120
- npm i -g qingflow-mcp@0.3.19
120
+ npm i -g qingflow-mcp@0.3.21
121
121
  ```
122
122
 
123
123
  Or one-click installer:
package/dist/server.js CHANGED
@@ -46,6 +46,9 @@ class InputValidationError extends Error {
46
46
  }
47
47
  const FORM_CACHE_TTL_MS = Number(process.env.QINGFLOW_FORM_CACHE_TTL_MS ?? "300000");
48
48
  const formCache = new Map();
49
+ const APPLY_APP_KEY_CACHE_TTL_MS = Number(process.env.QINGFLOW_APPLY_APP_KEY_CACHE_TTL_MS ?? "1800000");
50
+ const applyAppKeyCache = new Map();
51
+ const requestAppKeyCache = new Map();
49
52
  const CONTINUATION_CACHE_TTL_MS = Number(process.env.QINGFLOW_CONTINUATION_CACHE_TTL_MS ?? "900000");
50
53
  const continuationCache = new Map();
51
54
  const DEFAULT_PAGE_SIZE = 50;
@@ -65,7 +68,7 @@ const REQUEST_TIMEOUT_MS = toPositiveInt(process.env.QINGFLOW_REQUEST_TIMEOUT_MS
65
68
  const EXECUTION_BUDGET_MS = toPositiveInt(process.env.QINGFLOW_EXECUTION_BUDGET_MS) ?? 20000;
66
69
  const WAIT_RESULT_DEFAULT_TIMEOUT_MS = toPositiveInt(process.env.QINGFLOW_WAIT_RESULT_TIMEOUT_MS) ?? 5000;
67
70
  const WAIT_RESULT_POLL_INTERVAL_MS = toPositiveInt(process.env.QINGFLOW_WAIT_RESULT_POLL_INTERVAL_MS) ?? 500;
68
- const SERVER_VERSION = "0.3.19";
71
+ const SERVER_VERSION = "0.3.21";
69
72
  const accessToken = process.env.QINGFLOW_ACCESS_TOKEN;
70
73
  const baseUrl = process.env.QINGFLOW_BASE_URL;
71
74
  if (!accessToken) {
@@ -745,12 +748,14 @@ const listOutputSchema = listSuccessOutputSchema;
745
748
  const recordGetInputPublicSchema = z
746
749
  .object({
747
750
  apply_id: publicFieldSelectorSchema,
751
+ app_key: publicStringSchema.optional(),
748
752
  max_columns: z.number().int().positive().max(MAX_COLUMN_LIMIT).optional(),
749
753
  select_columns: z.array(publicFieldSelectorSchema).min(1).max(MAX_COLUMN_LIMIT),
750
754
  output_profile: outputProfileSchema.optional()
751
755
  });
752
756
  const recordGetInputSchema = z.preprocess(normalizeRecordGetInput, z.object({
753
757
  apply_id: z.union([z.string().min(1), z.number().int()]),
758
+ app_key: z.string().min(1).optional(),
754
759
  max_columns: z.number().int().positive().max(MAX_COLUMN_LIMIT).optional(),
755
760
  select_columns: z
756
761
  .array(z.union([z.string().min(1), z.number().int()]))
@@ -2404,6 +2409,8 @@ server.registerTool("qf_record_create", {
2404
2409
  const result = asObject(response.result);
2405
2410
  const requestId = asNullableString(result?.requestId);
2406
2411
  const immediateApplyId = result?.applyId ?? null;
2412
+ rememberRequestAppKey(requestId, parsedArgs.app_key);
2413
+ rememberApplyAppKey(immediateApplyId, parsedArgs.app_key);
2407
2414
  const shouldWaitForResult = (parsedArgs.wait_result ?? false) && requestId !== null && immediateApplyId === null;
2408
2415
  let finalApplyId = immediateApplyId;
2409
2416
  let waitStatus = immediateApplyId !== null ? "completed" : "pending";
@@ -2416,6 +2423,7 @@ server.registerTool("qf_record_create", {
2416
2423
  waitStatus = waited.status;
2417
2424
  rawOperationResult = waited.operationResult;
2418
2425
  finalApplyId = waited.applyId;
2426
+ rememberApplyAppKey(waited.applyId, parsedArgs.app_key);
2419
2427
  }
2420
2428
  const createResource = finalApplyId !== null ? { type: "record", apply_id: finalApplyId } : null;
2421
2429
  const createNextAction = waitStatus === "pending" || waitStatus === "timeout"
@@ -2476,6 +2484,8 @@ server.registerTool("qf_record_update", {
2476
2484
  const response = await client.updateRecord(String(parsedArgs.apply_id), { answers: normalizedAnswers }, { userId: parsedArgs.user_id });
2477
2485
  const result = asObject(response.result);
2478
2486
  const updateRequestId = asNullableString(result?.requestId);
2487
+ rememberRequestAppKey(updateRequestId, parsedArgs.app_key ?? null);
2488
+ rememberApplyAppKey(parsedArgs.apply_id, parsedArgs.app_key ?? null);
2479
2489
  const shouldWaitForUpdate = (parsedArgs.wait_result ?? false) && updateRequestId !== null;
2480
2490
  let updateStatus = "pending";
2481
2491
  let updateRawOperationResult = null;
@@ -2491,6 +2501,7 @@ server.registerTool("qf_record_update", {
2491
2501
  if (waited.applyId !== null) {
2492
2502
  updateApplyId = waited.applyId;
2493
2503
  }
2504
+ rememberApplyAppKey(waited.applyId, parsedArgs.app_key ?? null);
2494
2505
  }
2495
2506
  else if (updateRequestId === null) {
2496
2507
  // No async operation — synchronous completion
@@ -2536,6 +2547,8 @@ server.registerTool("qf_operation_get", {
2536
2547
  }, async (args) => {
2537
2548
  try {
2538
2549
  const response = await client.getOperation(args.request_id);
2550
+ const cachedAppKey = getCachedRequestAppKey(args.request_id);
2551
+ rememberApplyAppKey(extractOperationApplyId(response.result), cachedAppKey);
2539
2552
  return okResult({
2540
2553
  ok: true,
2541
2554
  data: {
@@ -2832,6 +2845,69 @@ function buildMeta(response) {
2832
2845
  base_url: baseUrl
2833
2846
  };
2834
2847
  }
2848
+ function rememberApplyAppKey(applyId, appKey) {
2849
+ const normalizedApplyId = asNullableString(applyId)?.trim();
2850
+ const normalizedAppKey = asNullableString(appKey)?.trim();
2851
+ if (!normalizedApplyId || !normalizedAppKey) {
2852
+ return;
2853
+ }
2854
+ applyAppKeyCache.set(normalizedApplyId, {
2855
+ appKey: normalizedAppKey,
2856
+ expiresAt: Date.now() + APPLY_APP_KEY_CACHE_TTL_MS
2857
+ });
2858
+ }
2859
+ function rememberRequestAppKey(requestId, appKey) {
2860
+ const normalizedRequestId = asNullableString(requestId)?.trim();
2861
+ const normalizedAppKey = asNullableString(appKey)?.trim();
2862
+ if (!normalizedRequestId || !normalizedAppKey) {
2863
+ return;
2864
+ }
2865
+ requestAppKeyCache.set(normalizedRequestId, {
2866
+ appKey: normalizedAppKey,
2867
+ expiresAt: Date.now() + APPLY_APP_KEY_CACHE_TTL_MS
2868
+ });
2869
+ }
2870
+ function getCachedApplyAppKey(applyId) {
2871
+ const normalizedApplyId = asNullableString(applyId)?.trim();
2872
+ if (!normalizedApplyId) {
2873
+ return null;
2874
+ }
2875
+ const hit = applyAppKeyCache.get(normalizedApplyId);
2876
+ if (!hit) {
2877
+ return null;
2878
+ }
2879
+ if (hit.expiresAt <= Date.now()) {
2880
+ applyAppKeyCache.delete(normalizedApplyId);
2881
+ return null;
2882
+ }
2883
+ return hit.appKey;
2884
+ }
2885
+ function getCachedRequestAppKey(requestId) {
2886
+ const normalizedRequestId = asNullableString(requestId)?.trim();
2887
+ if (!normalizedRequestId) {
2888
+ return null;
2889
+ }
2890
+ const hit = requestAppKeyCache.get(normalizedRequestId);
2891
+ if (!hit) {
2892
+ return null;
2893
+ }
2894
+ if (hit.expiresAt <= Date.now()) {
2895
+ requestAppKeyCache.delete(normalizedRequestId);
2896
+ return null;
2897
+ }
2898
+ return hit.appKey;
2899
+ }
2900
+ async function fetchRecordsByApplyIds(params) {
2901
+ const response = await client.listRecords(params.appKey, buildListPayload({
2902
+ pageNum: 1,
2903
+ pageSize: Math.min(Math.max(params.applyIds.length, 1), 200),
2904
+ applyIds: params.applyIds
2905
+ }), { userId: params.userId });
2906
+ const records = asArray(asObject(response.result)?.result)
2907
+ .map((item) => asObject(item))
2908
+ .filter((item) => Boolean(item));
2909
+ return { response, records };
2910
+ }
2835
2911
  function normalizeStringArray(value) {
2836
2912
  return uniqueStringList(asArray(value)
2837
2913
  .map((item) => asNullableString(item)?.trim() ?? "")
@@ -3783,6 +3859,7 @@ function normalizeRecordGetInput(raw) {
3783
3859
  return {
3784
3860
  ...normalizedObj,
3785
3861
  apply_id: coerceNumberLike(normalizedObj.apply_id),
3862
+ app_key: coerceStringLike(normalizedObj.app_key),
3786
3863
  max_columns: coerceNumberLike(normalizedObj.max_columns),
3787
3864
  select_columns: normalizeSelectorListInput(selectColumns),
3788
3865
  output_profile: normalizeOutputProfileInput(normalizedObj.output_profile)
@@ -3903,6 +3980,7 @@ function normalizeBatchGetInput(raw) {
3903
3980
  const selectColumns = normalizedObj.select_columns ?? normalizedObj.keep_columns;
3904
3981
  return {
3905
3982
  ...normalizedObj,
3983
+ app_key: coerceStringLike(normalizedObj.app_key),
3906
3984
  apply_ids: normalizeIdArrayInput(normalizedObj.apply_ids),
3907
3985
  max_columns: coerceNumberLike(normalizedObj.max_columns),
3908
3986
  select_columns: normalizeSelectorListInput(selectColumns),
@@ -5503,6 +5581,7 @@ function buildRecordGetArgsFromQuery(args) {
5503
5581
  }
5504
5582
  return recordGetInputSchema.parse({
5505
5583
  apply_id: args.apply_id,
5584
+ app_key: args.app_key,
5506
5585
  max_columns: args.max_columns,
5507
5586
  select_columns: args.select_columns,
5508
5587
  output_profile: args.output_profile
@@ -5588,6 +5667,13 @@ async function executeQueryPlan(args) {
5588
5667
  };
5589
5668
  }
5590
5669
  async function executeRecordsBatchGet(args) {
5670
+ if (!args.app_key) {
5671
+ throw missingRequiredFieldError({
5672
+ field: "app_key",
5673
+ tool: "qf_records_batch_get",
5674
+ fixHint: "Provide app_key, for example: {\"app_key\":\"21b3d559\",\"apply_ids\":[\"...\"],\"select_columns\":[0]}."
5675
+ });
5676
+ }
5591
5677
  if (!args.select_columns?.length) {
5592
5678
  throw missingRequiredFieldError({
5593
5679
  field: "select_columns",
@@ -5605,25 +5691,24 @@ async function executeRecordsBatchGet(args) {
5605
5691
  const rows = [];
5606
5692
  const missingApplyIds = [];
5607
5693
  let metaResponse = null;
5694
+ const { response, records } = await fetchRecordsByApplyIds({
5695
+ appKey: args.app_key,
5696
+ applyIds: requestedApplyIds
5697
+ });
5698
+ metaResponse = buildMeta(response);
5699
+ const byApplyId = new Map(records.map((record) => [String(record.applyId ?? ""), record]));
5608
5700
  for (const applyId of requestedApplyIds) {
5609
- try {
5610
- const response = await client.getRecord(applyId);
5611
- metaResponse = metaResponse ?? buildMeta(response);
5612
- const record = asObject(response.result) ?? {};
5613
- rows.push(buildFlatRowFromAnswers({
5614
- applyId: record.applyId ?? applyId,
5615
- answers: asArray(record.answers),
5616
- selectedColumns: selectedColumnsForRow
5617
- }));
5618
- }
5619
- catch (error) {
5620
- if (error instanceof QingflowApiError &&
5621
- (error.httpStatus === 404 || error.errCode === 404)) {
5622
- missingApplyIds.push(applyId);
5623
- continue;
5624
- }
5625
- throw error;
5701
+ const record = byApplyId.get(String(applyId));
5702
+ if (!record) {
5703
+ missingApplyIds.push(applyId);
5704
+ continue;
5626
5705
  }
5706
+ rememberApplyAppKey(record.applyId ?? applyId, args.app_key);
5707
+ rows.push(buildFlatRowFromAnswers({
5708
+ applyId: record.applyId ?? applyId,
5709
+ answers: asArray(record.answers),
5710
+ selectedColumns: selectedColumnsForRow
5711
+ }));
5627
5712
  }
5628
5713
  const completeness = {
5629
5714
  result_amount: requestedApplyIds.length,
@@ -6042,6 +6127,9 @@ async function executeRecordsList(args) {
6042
6127
  const items = collectedRawItems
6043
6128
  .slice(0, listLimit.limit)
6044
6129
  .map((raw) => normalizeRecordItem(raw, includeAnswers));
6130
+ for (const item of items) {
6131
+ rememberApplyAppKey(item.apply_id, args.app_key);
6132
+ }
6045
6133
  const sourceItemsForRows = items.slice();
6046
6134
  const requestedSelectColumns = selectResolution.columns.map((item) => item.requested);
6047
6135
  const columnProjection = projectRecordItemsColumns({
@@ -6186,10 +6274,52 @@ async function executeRecordGet(args) {
6186
6274
  }
6187
6275
  const outputProfile = resolveOutputProfile(args.output_profile);
6188
6276
  const queryId = randomUUID();
6189
- const response = await client.getRecord(String(args.apply_id));
6190
- const record = asObject(response.result) ?? {};
6277
+ let response;
6278
+ let record = null;
6279
+ try {
6280
+ response = await client.getRecord(String(args.apply_id));
6281
+ record = asObject(response.result) ?? {};
6282
+ }
6283
+ catch (error) {
6284
+ const providerError = error instanceof QingflowApiError ? error : null;
6285
+ const shouldFallback = providerError && (providerError.httpStatus === 404 || providerError.errCode === 404 || providerError.errCode === 49304);
6286
+ if (!shouldFallback) {
6287
+ throw error;
6288
+ }
6289
+ const fallbackAppKey = args.app_key ?? getCachedApplyAppKey(args.apply_id);
6290
+ if (!fallbackAppKey) {
6291
+ throw new InputValidationError({
6292
+ message: `qf_record_get could not read apply_id \"${String(args.apply_id)}\" through the direct record endpoint`,
6293
+ errorCode: "APP_KEY_REQUIRED_FOR_RECORD_GET",
6294
+ fixHint: "Retry qf_record_get with app_key, or call qf_records_batch_get/qf_records_list/qf_record_update first so MCP can infer the app_key for this apply_id.",
6295
+ details: {
6296
+ apply_id: String(args.apply_id),
6297
+ provider_err_code: providerError?.errCode ?? null,
6298
+ provider_err_msg: providerError?.errMsg ?? null
6299
+ }
6300
+ });
6301
+ }
6302
+ const fallback = await fetchRecordsByApplyIds({
6303
+ appKey: fallbackAppKey,
6304
+ applyIds: [String(args.apply_id)]
6305
+ });
6306
+ response = fallback.response;
6307
+ record = fallback.records.find((item) => String(item.applyId ?? "") === String(args.apply_id)) ?? null;
6308
+ if (!record) {
6309
+ throw new InputValidationError({
6310
+ message: `Record \"${String(args.apply_id)}\" not found in app \"${fallbackAppKey}\"`,
6311
+ errorCode: "RECORD_NOT_FOUND",
6312
+ fixHint: "Confirm apply_id and app_key, or fetch the row via qf_records_list/qf_query first.",
6313
+ details: {
6314
+ apply_id: String(args.apply_id),
6315
+ app_key: fallbackAppKey
6316
+ }
6317
+ });
6318
+ }
6319
+ rememberApplyAppKey(record.applyId ?? args.apply_id, fallbackAppKey);
6320
+ }
6191
6321
  const projection = projectAnswersForOutput({
6192
- answers: asArray(record.answers),
6322
+ answers: asArray(record?.answers),
6193
6323
  maxColumns: args.max_columns,
6194
6324
  selectColumns: args.select_columns
6195
6325
  });
@@ -6197,8 +6327,8 @@ async function executeRecordGet(args) {
6197
6327
  ? (projection.selectedColumns ?? []).slice(0, args.max_columns)
6198
6328
  : projection.selectedColumns ?? [];
6199
6329
  const row = buildFlatRowFromAnswers({
6200
- applyId: record.applyId ?? null,
6201
- answers: asArray(record.answers),
6330
+ applyId: record?.applyId ?? null,
6331
+ answers: asArray(record?.answers),
6202
6332
  selectedColumns: selectedColumnsForRow
6203
6333
  });
6204
6334
  const completeness = {
@@ -6219,11 +6349,12 @@ async function executeRecordGet(args) {
6219
6349
  apply_id: String(args.apply_id),
6220
6350
  selected_columns: selectedColumnsForRow
6221
6351
  };
6352
+ rememberApplyAppKey(record?.applyId ?? args.apply_id, args.app_key ?? getCachedApplyAppKey(args.apply_id));
6222
6353
  return {
6223
6354
  payload: {
6224
6355
  ok: true,
6225
6356
  data: {
6226
- apply_id: record.applyId ?? null,
6357
+ apply_id: record?.applyId ?? null,
6227
6358
  row,
6228
6359
  applied_limits: {
6229
6360
  column_cap: args.max_columns ?? null,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qingflow-mcp",
3
- "version": "0.3.19",
3
+ "version": "0.3.21",
4
4
  "private": false,
5
5
  "license": "MIT",
6
6
  "type": "module",