qingflow-mcp 0.3.16 → 0.3.18

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
@@ -3,6 +3,10 @@
3
3
  This MCP server wraps Qingflow OpenAPI for:
4
4
 
5
5
  - `qf_apps_list`
6
+ - `qf_departments_list`
7
+ - `qf_department_users_list`
8
+ - `qf_users_list`
9
+ - `qf_user_get`
6
10
  - `qf_form_get`
7
11
  - `qf_field_resolve`
8
12
  - `qf_query_plan`
@@ -108,7 +112,7 @@ npm i -g git+https://github.com/853046310/qingflow-mcp.git
108
112
  Install from npm (pinned version):
109
113
 
110
114
  ```bash
111
- npm i -g qingflow-mcp@0.3.14
115
+ npm i -g qingflow-mcp@0.3.18
112
116
  ```
113
117
 
114
118
  Or one-click installer:
@@ -148,6 +152,13 @@ MCP client config example:
148
152
  3. `qf_record_create` or `qf_record_update`.
149
153
  4. If create/update returns only `request_id`, call `qf_operation_get` to resolve async result.
150
154
 
155
+ Directory / org flow:
156
+
157
+ 1. `qf_departments_list` to inspect department tree.
158
+ 2. `qf_department_users_list` to inspect one department's members.
159
+ 3. `qf_users_list` for workspace-wide pagination.
160
+ 4. `qf_user_get` for one exact user.
161
+
151
162
  Full calling contract (Chinese):
152
163
 
153
164
  - [MCP 调用规范](./docs/MCP_CALLING_SPEC.md)
@@ -165,14 +176,45 @@ Full calling contract (Chinese):
165
176
  4. In `list` mode, `select_columns` is required.
166
177
  5. In `list` mode, row cap defaults to 200 when `max_rows` and `max_items` are omitted.
167
178
  6. In `record` mode, `select_columns` is required.
168
- 7. In `summary` mode, `select_columns` is required (`max_rows` defaults to 200 when omitted).
179
+ 7. In `summary` mode, `select_columns` is optional and can be auto-derived from `amount_column` / `time_range` (`max_rows` defaults to 200 when omitted).
169
180
 
170
181
  Summary mode output:
171
182
 
172
183
  1. `summary`: aggregated stats (`total_count`, `total_amount`, `by_day`, `missing_count`).
173
- 2. `rows`: strict column rows (only requested `select_columns`).
184
+ 2. `rows`: strict column rows (requested `select_columns`, or auto-derived preview columns when omitted).
174
185
  3. `meta`: field mapping, filter scope, stat policy, execution limits (`output_profile=verbose` only).
175
186
 
187
+ ## Directory / Org Tools
188
+
189
+ These tools expose department and member APIs without routing through `qf_query`:
190
+
191
+ 1. `qf_departments_list`
192
+ - optional `dept_id`
193
+ - local `keyword`, `limit`, `offset`
194
+ - aliases: `deptId`, `department_id`, `departmentId`
195
+ 2. `qf_department_users_list`
196
+ - required `dept_id`, `fetch_child`
197
+ - local `keyword`, `limit`, `offset`
198
+ - aliases: `deptId`, `department_id`, `departmentId`, `fetchChild`
199
+ 3. `qf_users_list`
200
+ - required `page_num`, `page_size`
201
+ - aliases: `pageNum`, `pageSize`
202
+ 4. `qf_user_get`
203
+ - required `user_id`
204
+ - alias: `userId`
205
+
206
+ CLI examples:
207
+
208
+ ```bash
209
+ qingflow-mcp cli call qf_departments_list --args '{"keyword":"销售","limit":20}'
210
+
211
+ qingflow-mcp cli call qf_department_users_list --args '{"deptId":111,"fetchChild":true}'
212
+
213
+ qingflow-mcp cli call qf_users_list --args '{"pageNum":1,"pageSize":100}'
214
+
215
+ qingflow-mcp cli call qf_user_get --args '{"userId":"u_123"}'
216
+ ```
217
+
176
218
  Return shape:
177
219
 
178
220
  1. success: structured payload `{ "ok": true, "data": ... }` (`meta` only in `output_profile=verbose`)
@@ -33,6 +33,48 @@ export class QingflowClient {
33
33
  }
34
34
  });
35
35
  }
36
+ listDepartments(options = {}) {
37
+ return this.request({
38
+ method: "GET",
39
+ path: "/department",
40
+ options: {
41
+ query: {
42
+ deptId: options.deptId !== undefined && options.deptId !== null
43
+ ? String(options.deptId)
44
+ : undefined
45
+ }
46
+ }
47
+ });
48
+ }
49
+ listDepartmentUsers(deptId, options) {
50
+ return this.request({
51
+ method: "GET",
52
+ path: `/department/${encodeURIComponent(deptId)}/user`,
53
+ options: {
54
+ query: {
55
+ fetchChild: options.fetchChild
56
+ }
57
+ }
58
+ });
59
+ }
60
+ listUsers(options) {
61
+ return this.request({
62
+ method: "GET",
63
+ path: "/user",
64
+ options: {
65
+ query: {
66
+ pageNum: options.pageNum,
67
+ pageSize: options.pageSize
68
+ }
69
+ }
70
+ });
71
+ }
72
+ getUser(userId) {
73
+ return this.request({
74
+ method: "GET",
75
+ path: `/user/${encodeURIComponent(userId)}`
76
+ });
77
+ }
36
78
  getForm(appKey, options = {}) {
37
79
  return this.request({
38
80
  method: "GET",
package/dist/server.js CHANGED
@@ -65,7 +65,7 @@ const REQUEST_TIMEOUT_MS = toPositiveInt(process.env.QINGFLOW_REQUEST_TIMEOUT_MS
65
65
  const EXECUTION_BUDGET_MS = toPositiveInt(process.env.QINGFLOW_EXECUTION_BUDGET_MS) ?? 20000;
66
66
  const WAIT_RESULT_DEFAULT_TIMEOUT_MS = toPositiveInt(process.env.QINGFLOW_WAIT_RESULT_TIMEOUT_MS) ?? 5000;
67
67
  const WAIT_RESULT_POLL_INTERVAL_MS = toPositiveInt(process.env.QINGFLOW_WAIT_RESULT_POLL_INTERVAL_MS) ?? 500;
68
- const SERVER_VERSION = "0.3.16";
68
+ const SERVER_VERSION = "0.3.18";
69
69
  const accessToken = process.env.QINGFLOW_ACCESS_TOKEN;
70
70
  const baseUrl = process.env.QINGFLOW_BASE_URL;
71
71
  if (!accessToken) {
@@ -215,6 +215,125 @@ const appsSuccessOutputSchema = z.object({
215
215
  meta: apiMetaSchema
216
216
  });
217
217
  const appsOutputSchema = appsSuccessOutputSchema;
218
+ const publicDirectorySelectorSchema = z.union([z.string().min(1), z.number().int()]);
219
+ const departmentOutputSchema = z.object({
220
+ dept_id: z.number().int().nullable(),
221
+ name: z.string().nullable(),
222
+ parent_id: z.number().int().nullable(),
223
+ ordinal: z.number().int().nullable(),
224
+ dept_leader_ids: z.array(z.string())
225
+ });
226
+ const directoryUserSchema = z.object({
227
+ user_id: z.string().nullable(),
228
+ name: z.string().nullable(),
229
+ area_code: z.string().nullable(),
230
+ mobile_num: z.string().nullable(),
231
+ email: z.string().nullable(),
232
+ head_img: z.string().nullable(),
233
+ department_ids: z.array(z.string()),
234
+ role_ids: z.array(z.string()),
235
+ custom_role_ids: z.array(z.string()),
236
+ custom_department_ids: z.array(z.string()),
237
+ being_disabled: z.boolean().nullable(),
238
+ being_active: z.boolean().nullable(),
239
+ superior_id: z.string().nullable()
240
+ });
241
+ const departmentListInputPublicSchema = z.object({
242
+ dept_id: publicDirectorySelectorSchema.optional(),
243
+ deptId: publicDirectorySelectorSchema.optional(),
244
+ department_id: publicDirectorySelectorSchema.optional(),
245
+ departmentId: publicDirectorySelectorSchema.optional(),
246
+ keyword: z.string().min(1).optional(),
247
+ limit: z.number().int().positive().max(500).optional(),
248
+ offset: z.number().int().nonnegative().optional()
249
+ });
250
+ const departmentListInputSchema = z.preprocess(normalizeDepartmentListInput, z.object({
251
+ dept_id: z.union([z.string().min(1), z.number().int()]).optional(),
252
+ keyword: z.string().min(1).optional(),
253
+ limit: z.number().int().positive().max(500).optional(),
254
+ offset: z.number().int().nonnegative().optional()
255
+ }));
256
+ const departmentListOutputSchema = z.object({
257
+ ok: z.literal(true),
258
+ data: z.object({
259
+ total_departments: z.number().int().nonnegative(),
260
+ returned_departments: z.number().int().nonnegative(),
261
+ limit: z.number().int().positive(),
262
+ offset: z.number().int().nonnegative(),
263
+ dept_id_filter: z.union([z.string(), z.number(), z.null()]),
264
+ departments: z.array(departmentOutputSchema)
265
+ }),
266
+ meta: apiMetaSchema
267
+ });
268
+ const departmentUsersInputPublicSchema = z.object({
269
+ dept_id: publicDirectorySelectorSchema.optional(),
270
+ deptId: publicDirectorySelectorSchema.optional(),
271
+ department_id: publicDirectorySelectorSchema.optional(),
272
+ departmentId: publicDirectorySelectorSchema.optional(),
273
+ fetch_child: z.boolean().optional(),
274
+ fetchChild: z.boolean().optional(),
275
+ keyword: z.string().min(1).optional(),
276
+ limit: z.number().int().positive().max(500).optional(),
277
+ offset: z.number().int().nonnegative().optional()
278
+ });
279
+ const departmentUsersInputSchema = z.preprocess(normalizeDepartmentUsersInput, z.object({
280
+ dept_id: z.union([z.string().min(1), z.number().int()]).optional(),
281
+ fetch_child: z.boolean().optional(),
282
+ keyword: z.string().min(1).optional(),
283
+ limit: z.number().int().positive().max(500).optional(),
284
+ offset: z.number().int().nonnegative().optional()
285
+ }));
286
+ const departmentUsersOutputSchema = z.object({
287
+ ok: z.literal(true),
288
+ data: z.object({
289
+ dept_id: z.string(),
290
+ fetch_child: z.boolean(),
291
+ leader_ids: z.array(z.string()),
292
+ total_users: z.number().int().nonnegative(),
293
+ returned_users: z.number().int().nonnegative(),
294
+ limit: z.number().int().positive(),
295
+ offset: z.number().int().nonnegative(),
296
+ users: z.array(directoryUserSchema)
297
+ }),
298
+ meta: apiMetaSchema
299
+ });
300
+ const usersListInputPublicSchema = z.object({
301
+ page_num: z.number().int().positive().optional(),
302
+ pageNum: z.number().int().positive().optional(),
303
+ page_size: z.number().int().positive().optional(),
304
+ pageSize: z.number().int().positive().optional()
305
+ });
306
+ const usersListInputSchema = z.preprocess(normalizeUsersListInput, z.object({
307
+ page_num: z.number().int().positive().optional(),
308
+ page_size: z.number().int().positive().optional()
309
+ }));
310
+ const usersListOutputSchema = z.object({
311
+ ok: z.literal(true),
312
+ data: z.object({
313
+ pagination: z.object({
314
+ page_num: z.number().int().positive(),
315
+ page_size: z.number().int().positive(),
316
+ page_amount: z.number().int().nonnegative(),
317
+ result_amount: z.number().int().nonnegative()
318
+ }),
319
+ users: z.array(directoryUserSchema)
320
+ }),
321
+ meta: apiMetaSchema
322
+ });
323
+ const userGetInputPublicSchema = z.object({
324
+ user_id: z.string().min(1).optional(),
325
+ userId: z.string().min(1).optional()
326
+ });
327
+ const userGetInputSchema = z.preprocess(normalizeUserGetInput, z.object({
328
+ user_id: z.string().min(1).optional()
329
+ }));
330
+ const userGetOutputSchema = z.object({
331
+ ok: z.literal(true),
332
+ data: z.object({
333
+ user: directoryUserSchema
334
+ }),
335
+ meta: apiMetaSchema
336
+ });
218
337
  const formInputSchema = z.object({
219
338
  app_key: z.string().min(1),
220
339
  user_id: z.string().min(1).optional(),
@@ -517,12 +636,23 @@ const createInputSchema = z
517
636
  const createSuccessOutputSchema = z.object({
518
637
  ok: z.literal(true),
519
638
  data: z.object({
639
+ status: z.enum(["completed", "pending", "timeout", "failed"]),
520
640
  request_id: z.string().nullable(),
521
641
  apply_id: z.union([z.string(), z.number(), z.null()]),
522
- resolved: z.boolean().optional(),
523
- timed_out: z.boolean().optional(),
524
- operation_result: z.unknown().optional(),
525
- async_hint: z.string()
642
+ resource: z
643
+ .object({
644
+ type: z.literal("record"),
645
+ apply_id: z.union([z.string(), z.number()])
646
+ })
647
+ .nullable(),
648
+ next_action: z
649
+ .object({
650
+ tool: z.string(),
651
+ arguments: z.record(z.unknown()),
652
+ reason: z.string().optional()
653
+ })
654
+ .nullable(),
655
+ raw: z.object({ operation_result: z.unknown() }).nullable()
526
656
  }),
527
657
  meta: apiMetaSchema
528
658
  });
@@ -555,12 +685,23 @@ const updateInputSchema = z
555
685
  const updateSuccessOutputSchema = z.object({
556
686
  ok: z.literal(true),
557
687
  data: z.object({
688
+ status: z.enum(["completed", "pending", "timeout", "failed"]),
558
689
  request_id: z.string().nullable(),
559
- apply_id: z.union([z.string(), z.number(), z.null()]).optional(),
560
- resolved: z.boolean().optional(),
561
- timed_out: z.boolean().optional(),
562
- operation_result: z.unknown().optional(),
563
- async_hint: z.string()
690
+ apply_id: z.union([z.string(), z.number(), z.null()]),
691
+ resource: z
692
+ .object({
693
+ type: z.literal("record"),
694
+ apply_id: z.union([z.string(), z.number()])
695
+ })
696
+ .nullable(),
697
+ next_action: z
698
+ .object({
699
+ tool: z.string(),
700
+ arguments: z.record(z.unknown()),
701
+ reason: z.string().optional()
702
+ })
703
+ .nullable(),
704
+ raw: z.object({ operation_result: z.unknown() }).nullable()
564
705
  }),
565
706
  meta: apiMetaSchema
566
707
  });
@@ -1277,6 +1418,219 @@ server.registerTool("qf_apps_list", {
1277
1418
  return errorResult(error);
1278
1419
  }
1279
1420
  });
1421
+ server.registerTool("qf_departments_list", {
1422
+ title: "Qingflow Departments List",
1423
+ description: "List departments with optional dept_id filter and local keyword slicing.",
1424
+ inputSchema: departmentListInputPublicSchema,
1425
+ outputSchema: departmentListOutputSchema,
1426
+ annotations: {
1427
+ readOnlyHint: true,
1428
+ idempotentHint: true
1429
+ }
1430
+ }, async (args) => {
1431
+ try {
1432
+ const parsedArgs = departmentListInputSchema.parse(args);
1433
+ let response;
1434
+ try {
1435
+ response = await client.listDepartments(parsedArgs.dept_id !== undefined ? { deptId: parsedArgs.dept_id } : {});
1436
+ }
1437
+ catch (error) {
1438
+ if (parsedArgs.dept_id !== undefined) {
1439
+ throw translateDirectoryApiError(error, {
1440
+ tool: "qf_departments_list",
1441
+ entity: "department",
1442
+ deptId: parsedArgs.dept_id
1443
+ });
1444
+ }
1445
+ throw error;
1446
+ }
1447
+ const keyword = parsedArgs.keyword?.trim().toLowerCase() ?? null;
1448
+ const limit = parsedArgs.limit ?? 50;
1449
+ const offset = parsedArgs.offset ?? 0;
1450
+ const departments = asArray(asObject(response.result)?.department).map((item) => normalizeDepartment(item));
1451
+ const filtered = keyword
1452
+ ? departments.filter((item) => (item.name ?? "").toLowerCase().includes(keyword) ||
1453
+ String(item.dept_id ?? "").toLowerCase().includes(keyword))
1454
+ : departments;
1455
+ const sliced = filtered.slice(offset, offset + limit);
1456
+ return okResult({
1457
+ ok: true,
1458
+ data: {
1459
+ total_departments: filtered.length,
1460
+ returned_departments: sliced.length,
1461
+ limit,
1462
+ offset,
1463
+ dept_id_filter: parsedArgs.dept_id ?? null,
1464
+ departments: sliced
1465
+ },
1466
+ meta: buildMeta(response)
1467
+ }, `Returned ${sliced.length}/${filtered.length} departments`);
1468
+ }
1469
+ catch (error) {
1470
+ return errorResult(error);
1471
+ }
1472
+ });
1473
+ server.registerTool("qf_department_users_list", {
1474
+ title: "Qingflow Department Users List",
1475
+ description: "List department members with optional child recursion and local keyword slicing.",
1476
+ inputSchema: departmentUsersInputPublicSchema,
1477
+ outputSchema: departmentUsersOutputSchema,
1478
+ annotations: {
1479
+ readOnlyHint: true,
1480
+ idempotentHint: true
1481
+ }
1482
+ }, async (args) => {
1483
+ try {
1484
+ const parsedArgs = departmentUsersInputSchema.parse(args);
1485
+ if (parsedArgs.dept_id === undefined) {
1486
+ throw missingRequiredFieldError({
1487
+ field: "dept_id",
1488
+ tool: "qf_department_users_list",
1489
+ fixHint: "Provide dept_id (or deptId), for example: {\"dept_id\":111,\"fetch_child\":false}."
1490
+ });
1491
+ }
1492
+ if (parsedArgs.fetch_child === undefined) {
1493
+ throw missingRequiredFieldError({
1494
+ field: "fetch_child",
1495
+ tool: "qf_department_users_list",
1496
+ fixHint: "Provide fetch_child as a native JSON boolean, for example: {\"dept_id\":111,\"fetch_child\":true}."
1497
+ });
1498
+ }
1499
+ let response;
1500
+ try {
1501
+ response = await client.listDepartmentUsers(String(parsedArgs.dept_id), {
1502
+ fetchChild: parsedArgs.fetch_child
1503
+ });
1504
+ }
1505
+ catch (error) {
1506
+ throw translateDirectoryApiError(error, {
1507
+ tool: "qf_department_users_list",
1508
+ entity: "department",
1509
+ deptId: parsedArgs.dept_id
1510
+ });
1511
+ }
1512
+ const keyword = parsedArgs.keyword?.trim().toLowerCase() ?? null;
1513
+ const limit = parsedArgs.limit ?? 200;
1514
+ const offset = parsedArgs.offset ?? 0;
1515
+ const result = asObject(response.result);
1516
+ const users = asArray(result?.userList).map((item) => normalizeUser(item));
1517
+ const filtered = keyword
1518
+ ? users.filter((item) => [item.user_id, item.name, item.email, item.mobile_num]
1519
+ .map((value) => (value ?? "").toLowerCase())
1520
+ .some((value) => value.includes(keyword)))
1521
+ : users;
1522
+ const sliced = filtered.slice(offset, offset + limit);
1523
+ return okResult({
1524
+ ok: true,
1525
+ data: {
1526
+ dept_id: String(parsedArgs.dept_id),
1527
+ fetch_child: parsedArgs.fetch_child,
1528
+ leader_ids: normalizeStringArray(result?.leaderIds),
1529
+ total_users: filtered.length,
1530
+ returned_users: sliced.length,
1531
+ limit,
1532
+ offset,
1533
+ users: sliced
1534
+ },
1535
+ meta: buildMeta(response)
1536
+ }, `Returned ${sliced.length}/${filtered.length} department users`);
1537
+ }
1538
+ catch (error) {
1539
+ return errorResult(error);
1540
+ }
1541
+ });
1542
+ server.registerTool("qf_users_list", {
1543
+ title: "Qingflow Users List",
1544
+ description: "List workspace users with explicit pagination.",
1545
+ inputSchema: usersListInputPublicSchema,
1546
+ outputSchema: usersListOutputSchema,
1547
+ annotations: {
1548
+ readOnlyHint: true,
1549
+ idempotentHint: true
1550
+ }
1551
+ }, async (args) => {
1552
+ try {
1553
+ const parsedArgs = usersListInputSchema.parse(args);
1554
+ if (parsedArgs.page_num === undefined) {
1555
+ throw missingRequiredFieldError({
1556
+ field: "page_num",
1557
+ tool: "qf_users_list",
1558
+ fixHint: "Provide page_num (or pageNum), for example: {\"page_num\":1,\"page_size\":100}."
1559
+ });
1560
+ }
1561
+ if (parsedArgs.page_size === undefined) {
1562
+ throw missingRequiredFieldError({
1563
+ field: "page_size",
1564
+ tool: "qf_users_list",
1565
+ fixHint: "Provide page_size (or pageSize), for example: {\"page_num\":1,\"page_size\":100}."
1566
+ });
1567
+ }
1568
+ const response = await client.listUsers({
1569
+ pageNum: parsedArgs.page_num,
1570
+ pageSize: parsedArgs.page_size
1571
+ });
1572
+ const result = asObject(response.result);
1573
+ const users = asArray(result?.result).map((item) => normalizeUser(item));
1574
+ return okResult({
1575
+ ok: true,
1576
+ data: {
1577
+ pagination: {
1578
+ page_num: toPositiveInt(result?.pageNum) ?? parsedArgs.page_num,
1579
+ page_size: toPositiveInt(result?.pageSize) ?? parsedArgs.page_size,
1580
+ page_amount: toNonNegativeInt(result?.pageAmount) ?? 0,
1581
+ result_amount: toNonNegativeInt(result?.resultAmount) ?? users.length
1582
+ },
1583
+ users
1584
+ },
1585
+ meta: buildMeta(response)
1586
+ }, `Returned ${users.length} users`);
1587
+ }
1588
+ catch (error) {
1589
+ return errorResult(error);
1590
+ }
1591
+ });
1592
+ server.registerTool("qf_user_get", {
1593
+ title: "Qingflow User Get",
1594
+ description: "Get one workspace user by user_id.",
1595
+ inputSchema: userGetInputPublicSchema,
1596
+ outputSchema: userGetOutputSchema,
1597
+ annotations: {
1598
+ readOnlyHint: true,
1599
+ idempotentHint: true
1600
+ }
1601
+ }, async (args) => {
1602
+ try {
1603
+ const parsedArgs = userGetInputSchema.parse(args);
1604
+ if (!parsedArgs.user_id) {
1605
+ throw missingRequiredFieldError({
1606
+ field: "user_id",
1607
+ tool: "qf_user_get",
1608
+ fixHint: "Provide user_id (or userId), for example: {\"user_id\":\"u_123\"}."
1609
+ });
1610
+ }
1611
+ let response;
1612
+ try {
1613
+ response = await client.getUser(parsedArgs.user_id);
1614
+ }
1615
+ catch (error) {
1616
+ throw translateDirectoryApiError(error, {
1617
+ tool: "qf_user_get",
1618
+ entity: "user",
1619
+ userId: parsedArgs.user_id
1620
+ });
1621
+ }
1622
+ return okResult({
1623
+ ok: true,
1624
+ data: {
1625
+ user: normalizeUser(response.result)
1626
+ },
1627
+ meta: buildMeta(response)
1628
+ }, `Fetched user ${parsedArgs.user_id}`);
1629
+ }
1630
+ catch (error) {
1631
+ return errorResult(error);
1632
+ }
1633
+ });
1280
1634
  server.registerTool("qf_form_get", {
1281
1635
  title: "Qingflow Form Get",
1282
1636
  description: "Get form metadata and compact field summaries for one app.",
@@ -1578,30 +1932,36 @@ server.registerTool("qf_record_create", {
1578
1932
  const immediateApplyId = result?.applyId ?? null;
1579
1933
  const shouldWaitForResult = (parsedArgs.wait_result ?? false) && requestId !== null && immediateApplyId === null;
1580
1934
  let finalApplyId = immediateApplyId;
1581
- let isResolved = immediateApplyId !== null;
1582
- let isTimedOut = false;
1583
- let operationResult = null;
1935
+ let waitStatus = immediateApplyId !== null ? "completed" : "pending";
1936
+ let rawOperationResult = null;
1584
1937
  if (shouldWaitForResult) {
1585
1938
  const waited = await waitForOperationResolution({
1586
1939
  requestId: requestId,
1587
1940
  timeoutMs: parsedArgs.wait_timeout_ms ?? WAIT_RESULT_DEFAULT_TIMEOUT_MS
1588
1941
  });
1589
- isResolved = waited.resolved;
1590
- isTimedOut = waited.timedOut;
1591
- operationResult = waited.operationResult;
1942
+ waitStatus = waited.status;
1943
+ rawOperationResult = waited.operationResult;
1592
1944
  finalApplyId = waited.applyId;
1593
1945
  }
1946
+ const createResource = finalApplyId !== null ? { type: "record", apply_id: finalApplyId } : null;
1947
+ const createNextAction = waitStatus === "pending" || waitStatus === "timeout"
1948
+ ? {
1949
+ tool: "qf_operation_get",
1950
+ arguments: { request_id: requestId },
1951
+ reason: waitStatus === "timeout"
1952
+ ? "Operation timed out; poll again to check completion."
1953
+ : "Operation is async; poll to check completion."
1954
+ }
1955
+ : null;
1594
1956
  return okResult({
1595
1957
  ok: true,
1596
1958
  data: {
1959
+ status: waitStatus,
1597
1960
  request_id: requestId,
1598
1961
  apply_id: finalApplyId,
1599
- resolved: isResolved,
1600
- ...(isTimedOut ? { timed_out: true } : {}),
1601
- ...(operationResult !== null ? { operation_result: operationResult } : {}),
1602
- async_hint: isResolved
1603
- ? "Record created and resolved."
1604
- : "Use qf_operation_get with request_id to fetch the result."
1962
+ resource: createResource,
1963
+ next_action: createNextAction,
1964
+ raw: rawOperationResult !== null ? { operation_result: rawOperationResult } : null
1605
1965
  },
1606
1966
  meta: buildMeta(response)
1607
1967
  }, `Create request sent for app ${parsedArgs.app_key}`);
@@ -1624,7 +1984,11 @@ server.registerTool("qf_record_update", {
1624
1984
  const parsedArgs = updateInputSchema.parse(args);
1625
1985
  const requiresForm = needsFormResolution(parsedArgs.fields);
1626
1986
  if (requiresForm && !parsedArgs.app_key) {
1627
- throw new Error("app_key is required when fields uses title-based keys");
1987
+ throw missingRequiredFieldError({
1988
+ field: "app_key",
1989
+ tool: "qf_record_update",
1990
+ fixHint: "Provide app_key when fields uses title-based keys, or switch fields to numeric que_id."
1991
+ });
1628
1992
  }
1629
1993
  const form = requiresForm && parsedArgs.app_key
1630
1994
  ? await getFormCached(parsedArgs.app_key, parsedArgs.user_id, Boolean(parsedArgs.force_refresh_form))
@@ -1639,31 +2003,45 @@ server.registerTool("qf_record_update", {
1639
2003
  const result = asObject(response.result);
1640
2004
  const updateRequestId = asNullableString(result?.requestId);
1641
2005
  const shouldWaitForUpdate = (parsedArgs.wait_result ?? false) && updateRequestId !== null;
1642
- let updateIsResolved = false;
1643
- let updateIsTimedOut = false;
1644
- let updateOperationResult = null;
1645
- let updateApplyId = null;
2006
+ let updateStatus = "pending";
2007
+ let updateRawOperationResult = null;
2008
+ let updateApplyId = parsedArgs.apply_id;
1646
2009
  if (shouldWaitForUpdate) {
1647
2010
  const waited = await waitForOperationResolution({
1648
2011
  requestId: updateRequestId,
1649
2012
  timeoutMs: parsedArgs.wait_timeout_ms ?? WAIT_RESULT_DEFAULT_TIMEOUT_MS
1650
2013
  });
1651
- updateIsResolved = waited.resolved;
1652
- updateIsTimedOut = waited.timedOut;
1653
- updateOperationResult = waited.operationResult;
1654
- updateApplyId = waited.applyId;
2014
+ updateStatus = waited.status;
2015
+ updateRawOperationResult = waited.operationResult;
2016
+ // For updates, the apply_id is already known from input; keep it unless operation returned a different one
2017
+ if (waited.applyId !== null) {
2018
+ updateApplyId = waited.applyId;
2019
+ }
2020
+ }
2021
+ else if (updateRequestId === null) {
2022
+ // No async operation — synchronous completion
2023
+ updateStatus = "completed";
1655
2024
  }
2025
+ // else: wait_result=false but has requestId — submitted, not polled, stays "pending"
2026
+ const updateResource = updateApplyId !== null ? { type: "record", apply_id: updateApplyId } : null;
2027
+ const updateNextAction = updateStatus === "pending" || updateStatus === "timeout"
2028
+ ? {
2029
+ tool: "qf_operation_get",
2030
+ arguments: { request_id: updateRequestId },
2031
+ reason: updateStatus === "timeout"
2032
+ ? "Operation timed out; poll again to check completion."
2033
+ : "Operation is async; poll to check completion."
2034
+ }
2035
+ : null;
1656
2036
  return okResult({
1657
2037
  ok: true,
1658
2038
  data: {
2039
+ status: updateStatus,
1659
2040
  request_id: updateRequestId,
1660
- ...(updateApplyId !== null ? { apply_id: updateApplyId } : {}),
1661
- resolved: updateIsResolved,
1662
- ...(updateIsTimedOut ? { timed_out: true } : {}),
1663
- ...(updateOperationResult !== null ? { operation_result: updateOperationResult } : {}),
1664
- async_hint: updateIsResolved
1665
- ? "Record updated and resolved."
1666
- : "Use qf_operation_get with request_id to fetch the update result."
2041
+ apply_id: updateApplyId,
2042
+ resource: updateResource,
2043
+ next_action: updateNextAction,
2044
+ raw: updateRawOperationResult !== null ? { operation_result: updateRawOperationResult } : null
1667
2045
  },
1668
2046
  meta: buildMeta(response)
1669
2047
  }, `Update request sent for apply ${String(parsedArgs.apply_id)}`);
@@ -1980,6 +2358,65 @@ function buildMeta(response) {
1980
2358
  base_url: baseUrl
1981
2359
  };
1982
2360
  }
2361
+ function normalizeStringArray(value) {
2362
+ return uniqueStringList(asArray(value)
2363
+ .map((item) => asNullableString(item)?.trim() ?? "")
2364
+ .filter((item) => item.length > 0));
2365
+ }
2366
+ function normalizeDepartment(raw) {
2367
+ const obj = asObject(raw) ?? {};
2368
+ return {
2369
+ dept_id: toNonNegativeInt(obj.deptId),
2370
+ name: asNullableString(obj.name),
2371
+ parent_id: toNonNegativeInt(obj.parentId),
2372
+ ordinal: toNonNegativeInt(obj.ordinal),
2373
+ dept_leader_ids: normalizeStringArray(obj.deptLeader)
2374
+ };
2375
+ }
2376
+ function normalizeUser(raw) {
2377
+ const obj = asObject(raw) ?? {};
2378
+ return {
2379
+ user_id: asNullableString(obj.userId),
2380
+ name: asNullableString(obj.name),
2381
+ area_code: asNullableString(obj.areaCode),
2382
+ mobile_num: asNullableString(obj.mobileNum),
2383
+ email: asNullableString(obj.email),
2384
+ head_img: asNullableString(obj.headImg),
2385
+ department_ids: normalizeStringArray(obj.department),
2386
+ role_ids: normalizeStringArray(obj.role),
2387
+ custom_role_ids: normalizeStringArray(obj.customRole),
2388
+ custom_department_ids: normalizeStringArray(obj.customDepartment),
2389
+ being_disabled: typeof obj.beingDisabled === "boolean" ? obj.beingDisabled : null,
2390
+ being_active: typeof obj.beingActive === "boolean" ? obj.beingActive : null,
2391
+ superior_id: asNullableString(obj.superiorId)
2392
+ };
2393
+ }
2394
+ function translateDirectoryApiError(error, params) {
2395
+ if (error instanceof QingflowApiError &&
2396
+ (error.httpStatus === 404 || error.errCode === 404)) {
2397
+ if (params.entity === "department") {
2398
+ return new InputValidationError({
2399
+ message: `Department \"${String(params.deptId)}\" not found`,
2400
+ errorCode: "DEPARTMENT_NOT_FOUND",
2401
+ fixHint: "Call qf_departments_list first to confirm the exact dept_id.",
2402
+ details: {
2403
+ tool: params.tool,
2404
+ dept_id: params.deptId
2405
+ }
2406
+ });
2407
+ }
2408
+ return new InputValidationError({
2409
+ message: `User \"${params.userId}\" not found`,
2410
+ errorCode: "USER_NOT_FOUND",
2411
+ fixHint: "Call qf_users_list or qf_department_users_list first to obtain a valid user_id.",
2412
+ details: {
2413
+ tool: params.tool,
2414
+ user_id: params.userId
2415
+ }
2416
+ });
2417
+ }
2418
+ return error instanceof Error ? error : new Error(String(error));
2419
+ }
1983
2420
  function resolveOutputProfile(value) {
1984
2421
  return value === "verbose" ? "verbose" : DEFAULT_OUTPUT_PROFILE;
1985
2422
  }
@@ -2075,6 +2512,76 @@ function normalizeToolSpecInput(raw) {
2075
2512
  include_all: coerceBooleanLike(normalizedObj.include_all)
2076
2513
  };
2077
2514
  }
2515
+ function normalizeDepartmentListInput(raw) {
2516
+ const parsedRoot = parseJsonLikeDeep(raw);
2517
+ const obj = asObject(parsedRoot);
2518
+ if (!obj) {
2519
+ return parsedRoot;
2520
+ }
2521
+ const normalizedObj = applyAliases(obj, {
2522
+ deptId: "dept_id",
2523
+ department_id: "dept_id",
2524
+ departmentId: "dept_id"
2525
+ });
2526
+ return {
2527
+ ...normalizedObj,
2528
+ dept_id: coerceNumberLike(normalizeSelectorInputValue(normalizedObj.dept_id)),
2529
+ keyword: coerceStringLike(normalizedObj.keyword),
2530
+ limit: coerceNumberLike(normalizedObj.limit),
2531
+ offset: coerceNumberLike(normalizedObj.offset)
2532
+ };
2533
+ }
2534
+ function normalizeDepartmentUsersInput(raw) {
2535
+ const parsedRoot = parseJsonLikeDeep(raw);
2536
+ const obj = asObject(parsedRoot);
2537
+ if (!obj) {
2538
+ return parsedRoot;
2539
+ }
2540
+ const normalizedObj = applyAliases(obj, {
2541
+ deptId: "dept_id",
2542
+ department_id: "dept_id",
2543
+ departmentId: "dept_id",
2544
+ fetchChild: "fetch_child"
2545
+ });
2546
+ return {
2547
+ ...normalizedObj,
2548
+ dept_id: coerceNumberLike(normalizeSelectorInputValue(normalizedObj.dept_id)),
2549
+ fetch_child: coerceBooleanLike(normalizedObj.fetch_child),
2550
+ keyword: coerceStringLike(normalizedObj.keyword),
2551
+ limit: coerceNumberLike(normalizedObj.limit),
2552
+ offset: coerceNumberLike(normalizedObj.offset)
2553
+ };
2554
+ }
2555
+ function normalizeUsersListInput(raw) {
2556
+ const parsedRoot = parseJsonLikeDeep(raw);
2557
+ const obj = asObject(parsedRoot);
2558
+ if (!obj) {
2559
+ return parsedRoot;
2560
+ }
2561
+ const normalizedObj = applyAliases(obj, {
2562
+ pageNum: "page_num",
2563
+ pageSize: "page_size"
2564
+ });
2565
+ return {
2566
+ ...normalizedObj,
2567
+ page_num: coerceNumberLike(normalizedObj.page_num),
2568
+ page_size: coerceNumberLike(normalizedObj.page_size)
2569
+ };
2570
+ }
2571
+ function normalizeUserGetInput(raw) {
2572
+ const parsedRoot = parseJsonLikeDeep(raw);
2573
+ const obj = asObject(parsedRoot);
2574
+ if (!obj) {
2575
+ return parsedRoot;
2576
+ }
2577
+ const normalizedObj = applyAliases(obj, {
2578
+ userId: "user_id"
2579
+ });
2580
+ return {
2581
+ ...normalizedObj,
2582
+ user_id: coerceStringLike(normalizedObj.user_id)
2583
+ };
2584
+ }
2078
2585
  function buildToolSpecCatalog() {
2079
2586
  return [
2080
2587
  {
@@ -2104,6 +2611,63 @@ function buildToolSpecCatalog() {
2104
2611
  limit: 20
2105
2612
  }
2106
2613
  },
2614
+ {
2615
+ tool: "qf_departments_list",
2616
+ required: [],
2617
+ limits: {
2618
+ limit_max: 500,
2619
+ offset_min: 0,
2620
+ input_contract: "strict JSON only; dept_id must be a native JSON string or number when provided"
2621
+ },
2622
+ aliases: collectAliasHints(["dept_id"], {
2623
+ dept_id: ["deptId", "department_id", "departmentId"]
2624
+ }),
2625
+ minimal_example: {
2626
+ dept_id: 111,
2627
+ limit: 20
2628
+ }
2629
+ },
2630
+ {
2631
+ tool: "qf_department_users_list",
2632
+ required: ["dept_id", "fetch_child"],
2633
+ limits: {
2634
+ limit_max: 500,
2635
+ offset_min: 0,
2636
+ input_contract: "strict JSON only; fetch_child must be a native JSON boolean"
2637
+ },
2638
+ aliases: collectAliasHints(["dept_id", "fetch_child"], {
2639
+ dept_id: ["deptId", "department_id", "departmentId"],
2640
+ fetch_child: ["fetchChild"]
2641
+ }),
2642
+ minimal_example: {
2643
+ dept_id: 111,
2644
+ fetch_child: true,
2645
+ limit: 50
2646
+ }
2647
+ },
2648
+ {
2649
+ tool: "qf_users_list",
2650
+ required: ["page_num", "page_size"],
2651
+ limits: {
2652
+ input_contract: "strict JSON only; page_num/page_size must be native JSON numbers"
2653
+ },
2654
+ aliases: collectAliasHints(["page_num", "page_size"], {}),
2655
+ minimal_example: {
2656
+ page_num: 1,
2657
+ page_size: 100
2658
+ }
2659
+ },
2660
+ {
2661
+ tool: "qf_user_get",
2662
+ required: ["user_id"],
2663
+ limits: {
2664
+ input_contract: "strict JSON only; user_id must be a native JSON string"
2665
+ },
2666
+ aliases: collectAliasHints(["user_id"], {}),
2667
+ minimal_example: {
2668
+ user_id: "u_123"
2669
+ }
2670
+ },
2107
2671
  {
2108
2672
  tool: "qf_form_get",
2109
2673
  required: ["app_key"],
@@ -3362,7 +3926,7 @@ function inferPlanMissingRequired(tool, args) {
3362
3926
  missing.push("select_columns");
3363
3927
  }
3364
3928
  }
3365
- else {
3929
+ else if (queryMode === "list") {
3366
3930
  if (!hasAppKey) {
3367
3931
  missing.push("app_key");
3368
3932
  }
@@ -3370,6 +3934,9 @@ function inferPlanMissingRequired(tool, args) {
3370
3934
  missing.push("select_columns");
3371
3935
  }
3372
3936
  }
3937
+ else if (!hasAppKey) {
3938
+ missing.push("app_key");
3939
+ }
3373
3940
  }
3374
3941
  return missing;
3375
3942
  }
@@ -4516,12 +5083,12 @@ async function executeRecordsExport(format, args) {
4516
5083
  app_key: args.app_key,
4517
5084
  selected_columns: selectedColumnsForRows,
4518
5085
  filters: echoFilters(effectiveFilters),
4519
- time_range: args.time_range
5086
+ time_range: timeRangeResolution.mapping
4520
5087
  ? {
4521
- column: String(args.time_range.column),
4522
- from: args.time_range.from ?? null,
4523
- to: args.time_range.to ?? null,
4524
- timezone: args.time_range.timezone ?? null
5088
+ column: String(args.time_range?.column ?? timeRangeResolution.mapping.requested ?? ""),
5089
+ from: timeRangeResolution.time_range?.from ?? null,
5090
+ to: timeRangeResolution.time_range?.to ?? null,
5091
+ timezone: timeRangeResolution.time_range?.timezone ?? null
4525
5092
  }
4526
5093
  : null
4527
5094
  }, sourcePages);
@@ -4547,7 +5114,6 @@ async function executeRecordsExport(format, args) {
4547
5114
  ? {
4548
5115
  completeness,
4549
5116
  evidence,
4550
- resolved_mappings: resolvedMappings,
4551
5117
  execution: {
4552
5118
  scanned_pages: fetchedPages,
4553
5119
  requested_pages: requestedPages,
@@ -4562,6 +5128,7 @@ async function executeRecordsExport(format, args) {
4562
5128
  ? {
4563
5129
  completeness,
4564
5130
  evidence,
5131
+ resolved_mappings: resolvedMappings,
4565
5132
  error_code: null,
4566
5133
  fix_hint: null
4567
5134
  }
@@ -6441,6 +7008,13 @@ function isPendingOperationStatus(status) {
6441
7008
  return ["PENDING", "PROCESSING", "RUNNING", "IN_PROGRESS", "QUEUED"].includes(status);
6442
7009
  }
6443
7010
  function extractOperationApplyId(operationResult) {
7011
+ // Handle case where operationResult itself is a numeric string or number (the apply_id directly)
7012
+ if (typeof operationResult === "string" && /^\d+$/.test(operationResult.trim())) {
7013
+ return operationResult.trim();
7014
+ }
7015
+ if (typeof operationResult === "number" && Number.isFinite(operationResult)) {
7016
+ return operationResult;
7017
+ }
6444
7018
  const obj = asObject(operationResult);
6445
7019
  return obj?.applyId ?? obj?.apply_id ?? null;
6446
7020
  }
@@ -6450,15 +7024,14 @@ async function waitForOperationResolution(params) {
6450
7024
  while (Date.now() <= deadline) {
6451
7025
  const response = await client.getOperation(params.requestId);
6452
7026
  lastResult = response.result;
6453
- const status = extractOperationStatus(lastResult);
7027
+ const opStatus = extractOperationStatus(lastResult);
6454
7028
  const applyId = extractOperationApplyId(lastResult);
6455
- if ((status && !isPendingOperationStatus(status)) || applyId !== null) {
6456
- return {
6457
- resolved: true,
6458
- timedOut: false,
6459
- operationResult: lastResult,
6460
- applyId
6461
- };
7029
+ if (applyId !== null) {
7030
+ return { status: "completed", operationResult: lastResult, applyId };
7031
+ }
7032
+ if (opStatus && !isPendingOperationStatus(opStatus)) {
7033
+ // Non-pending status but no apply_id — treat as failed
7034
+ return { status: "failed", operationResult: lastResult, applyId: null };
6462
7035
  }
6463
7036
  const remaining = deadline - Date.now();
6464
7037
  if (remaining <= 0) {
@@ -6466,12 +7039,12 @@ async function waitForOperationResolution(params) {
6466
7039
  }
6467
7040
  await delay(Math.min(WAIT_RESULT_POLL_INTERVAL_MS, remaining));
6468
7041
  }
6469
- return {
6470
- resolved: false,
6471
- timedOut: true,
6472
- operationResult: lastResult,
6473
- applyId: extractOperationApplyId(lastResult)
6474
- };
7042
+ // Timed out — check if last result has apply_id anyway (edge case)
7043
+ const finalApplyId = extractOperationApplyId(lastResult);
7044
+ if (finalApplyId !== null) {
7045
+ return { status: "completed", operationResult: lastResult, applyId: finalApplyId };
7046
+ }
7047
+ return { status: "timeout", operationResult: lastResult, applyId: null };
6475
7048
  }
6476
7049
  function resolveListItemLimit(params) {
6477
7050
  if (params.total <= 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qingflow-mcp",
3
- "version": "0.3.16",
3
+ "version": "0.3.18",
4
4
  "private": false,
5
5
  "license": "MIT",
6
6
  "type": "module",