@wukongcrm/mcp-server 0.2.5 → 0.2.6

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/dist/server.js CHANGED
@@ -2,14 +2,70 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { z } from "zod";
3
3
  import { CrmClient } from "./client.js";
4
4
  import { createNocodeToolHandlers, nocodeToolDefinitions } from "./nocode.js";
5
+ import { getToolDiscovery, TOOL_DISCOVERY_META_KEY } from "./tool-discovery.js";
5
6
  import { createToolHandlers } from "./tools.js";
6
7
  export const CRM_READ_SCOPE = "crm.read";
7
8
  export const CRM_WRITE_SCOPE = "crm.write";
8
9
  export const OFFLINE_ACCESS_SCOPE = "offline_access";
9
10
  export const MCP_RESOURCE_SCOPES = [CRM_READ_SCOPE, CRM_WRITE_SCOPE];
10
11
  export const OAUTH_SCOPES = [...MCP_RESOURCE_SCOPES, OFFLINE_ACCESS_SCOPE];
12
+ // 工具名称不能可靠表达副作用(例如 workorder_update_record、favourOrCancel)。
13
+ // 这里以显式清单作为 access/annotations 的唯一运行时来源;未列出的主工具一律按只读处理。
14
+ const WRITE_TOOL_SAFETY = {
15
+ crm_toggle_address_book_attention: { destructive: true },
16
+ crm_write_hrm: { destructive: true },
17
+ crm_create_hrm_record: { destructive: false },
18
+ crm_update_hrm_record: { destructive: true },
19
+ crm_write_finance: { destructive: true },
20
+ crm_create_finance_voucher: { destructive: false },
21
+ crm_update_finance_voucher: { destructive: true },
22
+ crm_write_jxc: { destructive: true },
23
+ crm_create_jxc_record: { destructive: false },
24
+ crm_update_jxc_record: { destructive: true },
25
+ crm_create_oa_announcement: { destructive: false },
26
+ crm_update_oa_announcement: { destructive: true },
27
+ crm_delete_oa_announcement: { destructive: true },
28
+ crm_mark_oa_announcement_read: { destructive: false },
29
+ crm_create_record: { destructive: false },
30
+ crm_update_customer: { destructive: true },
31
+ crm_update_record_fields: { destructive: true },
32
+ crm_create_product: { destructive: false },
33
+ crm_update_product: { destructive: true },
34
+ crm_update_product_fields: { destructive: true },
35
+ crm_update_product_status: { destructive: true },
36
+ crm_transfer_products_owner: { destructive: true },
37
+ crm_delete_products: { destructive: true },
38
+ crm_create_contract: { destructive: false },
39
+ crm_update_contract: { destructive: true },
40
+ crm_create_receivables: { destructive: false },
41
+ crm_update_receivables: { destructive: true },
42
+ crm_create_receivables_plan: { destructive: false },
43
+ crm_update_receivables_plan: { destructive: true },
44
+ crm_create_invoice: { destructive: false },
45
+ crm_update_invoice: { destructive: true },
46
+ crm_create_quotation: { destructive: false },
47
+ crm_update_quotation: { destructive: true },
48
+ crm_create_calendar_event: { destructive: false },
49
+ crm_update_calendar_event: { destructive: true },
50
+ crm_create_oa_log: { destructive: false },
51
+ crm_update_oa_log: { destructive: true },
52
+ crm_delete_oa_log: { destructive: true },
53
+ crm_toggle_oa_log_favour: { destructive: true },
54
+ crm_create_oa_examine: { destructive: false },
55
+ crm_save_oa_examine_draft: { destructive: true },
56
+ crm_audit_oa_examine: { destructive: true },
57
+ crm_batch_audit_oa_examine: { destructive: true },
58
+ crm_create_oa_task: { destructive: false },
59
+ crm_update_oa_task_fields: { destructive: true },
60
+ crm_add_followup: { destructive: false },
61
+ crm_update_followup: { destructive: true },
62
+ workorder_create_record: { destructive: false },
63
+ workorder_update_record: { destructive: true }
64
+ };
11
65
  const moduleSchema = z.union([z.string(), z.number()]);
12
- const idSchema = z.union([z.string(), z.number()]);
66
+ const idSchema = z.string()
67
+ .regex(/^\d+$/, "ID 必须是数字字符串。")
68
+ .describe("ID 必须以数字字符串传入,禁止使用 JSON number,避免 Snowflake ID 精度丢失。");
13
69
  const passthroughSchema = z.object({}).passthrough();
14
70
  const toolOutputSchema = z.object({ result: z.unknown() });
15
71
  const searchFilterSchema = z.object({
@@ -21,17 +77,62 @@ const searchFilterSchema = z.object({
21
77
  z.number().int().min(1).max(14),
22
78
  z.enum(["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14"])
23
79
  ]).optional().describe("高级筛选操作码;与 type 同义,同时提供时必须一致。"),
24
- values: z.array(z.unknown()).optional().describe("筛选值数组。除为空/不为空操作 5/6 外必须提供;datetime 范围 14 使用 [YYYY-MM-DD HH:mm:ss, YYYY-MM-DD HH:mm:ss]。"),
80
+ values: z.array(z.unknown()).optional().describe("筛选值数组。ID 必须使用数字字符串;除为空/不为空操作 5/6 外必须提供;datetime 范围 14 使用 [YYYY-MM-DD HH:mm:ss, YYYY-MM-DD HH:mm:ss]。"),
25
81
  groupId: z.number().int().optional().describe("同组条件为 AND,不同组为 OR。")
26
82
  }).passthrough();
27
83
  const crmCreateRecordSchema = z.object({
28
84
  module: moduleSchema,
29
- entity: z.record(z.unknown()).optional().describe("CRM 实体字段;系统字段放在这里,例如 entity.customerName"),
85
+ entity: z.record(z.unknown()).optional().describe("CRM 实体字段;系统字段放在这里,例如 entity.customerName;所有 *Id/*Ids 字段使用数字字符串。"),
30
86
  field: z.array(z.record(z.unknown())).optional().describe("CRM 自定义字段列表。"),
31
87
  saveType: z.number().optional(),
32
88
  fileId: idSchema.optional(),
33
89
  aiAnalysis: z.number().optional(),
34
90
  member: z.record(z.unknown()).optional(),
91
+ product: z.array(z.unknown()).optional().describe("商机等保存接口的关联产品列表;这是顶层字段,不放入 entity。"),
92
+ contactsId: idSchema.optional().describe("商机主联系人 ID;这是顶层字段,不放入 entity。"),
93
+ customerSuperior: z.record(z.unknown()).optional().describe("上级客户信息;这是顶层字段。"),
94
+ contacts: z.record(z.unknown()).optional().describe("企业微信联系人信息;这是顶层字段。"),
95
+ examineFlowData: z.record(z.unknown()).optional().describe("审批数据;这是顶层字段。"),
96
+ confirm: z.boolean().optional()
97
+ }).strict();
98
+ const followupActivityTypeSchema = z.union([
99
+ z.literal(1), z.literal(2), z.literal(3), z.literal(4), z.literal(5), z.literal(6),
100
+ z.literal(7), z.literal(14), z.literal(17), z.literal(18), z.literal(22)
101
+ ]).describe("关联模块类型:1线索、2客户、3联系人、4产品、5商机、6合同、7回款、14回款计划、17回访、18发票、22报价单。");
102
+ const followupEntitySchema = z.object({
103
+ id: idSchema.optional().describe("更新跟进时的跟进记录 ID;必须使用数字字符串。"),
104
+ content: z.string().min(1).describe("跟进内容,不能为空。"),
105
+ category: z.string().min(1).describe("跟进方式/分类,不能为空。"),
106
+ activityType: followupActivityTypeSchema,
107
+ activityTypeId: idSchema.describe("被跟进 CRM 记录的 ID;必须使用数字字符串。"),
108
+ activityTypeName: z.string().optional(),
109
+ nextTime: z.string().optional(),
110
+ visitPlanId: idSchema.optional(),
111
+ remindTeamMembers: z.unknown().optional(),
112
+ atUserId: z.array(idSchema).optional(),
113
+ relateDataList: z.array(z.record(z.unknown())).optional()
114
+ }).passthrough();
115
+ const crmAddFollowupSchema = z.object({
116
+ entity: followupEntitySchema,
117
+ field: z.array(z.record(z.unknown())).optional(),
118
+ saveType: z.number().optional(),
119
+ fileId: idSchema.optional(),
120
+ aiAnalysis: z.number().optional(),
121
+ member: z.record(z.unknown()).optional(),
122
+ confirm: z.boolean().optional()
123
+ }).strict();
124
+ const crmUpdateFollowupSchema = z.object({
125
+ id: idSchema.optional(),
126
+ entity: followupEntitySchema.optional(),
127
+ field: z.array(z.record(z.unknown())).optional(),
128
+ saveType: z.number().optional(),
129
+ fileId: idSchema.optional(),
130
+ aiAnalysis: z.number().optional(),
131
+ member: z.record(z.unknown()).optional(),
132
+ useInformationUpdate: z.boolean().optional(),
133
+ list: z.array(z.record(z.unknown())).optional(),
134
+ fields: z.array(z.record(z.unknown())).optional(),
135
+ batchId: z.string().optional(),
35
136
  confirm: z.boolean().optional()
36
137
  }).strict();
37
138
  const crmUserSearchShape = {
@@ -50,15 +151,35 @@ const toolDefinitions = [
50
151
  ["crm_auth_status", "使用 CRM_API_KEY 自动换取 Admin-Token 后检查当前登录态,异常时提示检查 API Key。", z.object({}).optional()],
51
152
  ["crm_list_modules", "列出当前 MCP 固定支持的 72CRM 模块、label、路径和中英文操作边界。", z.object({}).optional()],
52
153
  ["crm_list_customer_pools", "查询当前账号有权限访问的客户公海,并统计每个公海下面有多少客户;只读,不领取、不分配、不流转。", z.object({ includeSamples: z.boolean().optional(), sampleLimit: z.number().optional() }).passthrough()],
53
- ["crm_get_module_schema", "获取模块字段结构和列表头,供本地 AI 判断字段怎么填。", z.object({ module: moduleSchema, id: z.union([z.string(), z.number()]).optional() }).passthrough()],
154
+ ["crm_get_module_schema", "获取模块字段结构和列表头,供本地 AI 判断字段怎么填。", z.object({ module: moduleSchema, id: idSchema.optional() }).passthrough()],
155
+ ["crm_get_business_status_groups", "查询当前账号可用的商机状态组及阶段。新增商机前必须调用;entity.typeId 取 flowId,entity.statusId 取同一组 settingList 中的 settingId。存在多个状态组且用户未指定时必须询问,不能猜测。", z.object({ businessId: idSchema.optional() }).strict()],
54
156
  ["crm_validate_field", "调用固定字段校验接口,校验字段值或联系人查重。", passthroughSchema],
55
157
  ["crm_search_records", "查询模块分页列表。人员便捷参数和 user 筛选会解析为用户 ID,并使用后端支持的操作 3;日期/日期时间范围筛选必须使用操作 14。查询某人员某天的跟进记录时,module=activity,人员使用 createUserId(s)/createUserName(s),createTime 使用 formType=datetime 和当天 00:00:00 至 23:59:59 的范围。", z.object({ module: moduleSchema, page: z.number().optional(), limit: z.number().optional(), ...crmUserSearchShape }).passthrough()],
56
- ["crm_get_record", "按模块和 ID 查询单条 CRM 记录详情。", z.object({ module: moduleSchema, id: z.union([z.string(), z.number()]) }).passthrough()],
57
- ["crm_get_record_information", "查询详情页字段展示数据,包含字段中文名和值。", z.object({ module: moduleSchema, id: z.union([z.string(), z.number()]) }).passthrough()],
58
- ["crm_get_related_records", "查询固定白名单内的关联数据,例如商机产品、联系人商机、合同回款计划、产品上架列表。", z.object({ module: moduleSchema, id: z.union([z.string(), z.number()]).optional(), relation: z.string() }).passthrough()],
59
- ["crm_get_record_timeline", "查询某条记录关联的跟进/活动时间线。", z.object({ module: moduleSchema, id: z.union([z.string(), z.number()]) }).passthrough()],
60
- ["crm_get_action_records", "查询某条记录的操作记录。", z.object({ module: moduleSchema, id: z.union([z.string(), z.number()]) }).passthrough()],
61
- ["crm_list_files", "只读查询附件元数据,不上传、不删除、不代理下载流。", z.object({ module: moduleSchema, id: z.union([z.string(), z.number()]) }).passthrough()],
158
+ ["crm_get_record", "按模块和 ID 查询单条 CRM 记录的完整对象;不返回详情页字段标签、记录动态、跟进列表或操作记录。", z.object({
159
+ module: moduleSchema.describe("CRM 模块 key、label 或别名;可先调用 crm_list_modules 获取。"),
160
+ id: idSchema.describe("目标 CRM 记录 ID;必须使用数字字符串。")
161
+ }).passthrough()],
162
+ ["crm_get_record_information", "查询 CRM 详情页字段展示数据,返回字段中文名和值;不返回完整记录对象、记录动态、跟进列表或操作记录。", z.object({
163
+ module: moduleSchema.describe("CRM 模块 key、label 或别名。"),
164
+ id: idSchema.describe("目标 CRM 记录 ID;必须使用数字字符串。")
165
+ }).passthrough()],
166
+ ["crm_get_related_records", "查询固定白名单内的 CRM 关联资料,例如商机产品、联系人商机、合同回款计划、产品上架列表;不查询记录动态、跟进记录或操作记录。", z.object({
167
+ module: moduleSchema.describe("CRM 模块 key、label 或别名。"),
168
+ id: idSchema.optional().describe("目标 CRM 记录 ID;具体 relation 不需要 ID 时可省略。"),
169
+ relation: z.string().describe("关联资料类型;必须是 MCP 固定白名单内的 relation。")
170
+ }).passthrough()],
171
+ ["crm_get_record_timeline", "查询某条 CRM 记录的系统动态时间线(如创建记录、字段变化、阶段变化);固定调用 /crmActivity/queryActivityList,不返回跟进记录列表,也不是操作审计记录。", z.object({
172
+ module: moduleSchema.describe("CRM 模块 key、label 或别名,用于确定动态的 activityType。"),
173
+ id: idSchema.describe("目标 CRM 记录 ID;必须使用数字字符串。"),
174
+ page: z.number().int().positive().optional().describe("动态页码,默认 1。"),
175
+ limit: z.number().int().positive().max(200).optional().describe("每页动态条数,默认 15。"),
176
+ extra: z.record(z.unknown()).optional().describe("透传给动态查询接口的附加只读筛选参数。")
177
+ }).passthrough()],
178
+ ["crm_get_action_records", "查询某条 CRM 记录的操作审计记录;不返回记录动态或跟进记录列表。", z.object({
179
+ module: moduleSchema.describe("CRM 模块 key、label 或别名。"),
180
+ id: idSchema.describe("目标 CRM 记录 ID;必须使用数字字符串。")
181
+ }).passthrough()],
182
+ ["crm_list_files", "只读查询附件元数据,不上传、不删除、不代理下载流。", z.object({ module: moduleSchema, id: idSchema }).passthrough()],
62
183
  ["crm_search_products", "只读查询 CRM 产品列表;固定调用 /crmProduct/queryPageList,支持 keyword/categoryId/status 以及负责人、创建人的用户 ID 或唯一昵称筛选。", z.object({ page: z.number().optional(), limit: z.number().optional(), keyword: z.string().optional(), categoryId: idSchema.optional(), status: z.union([z.string(), z.number()]).optional(), ...crmUserSearchShape }).passthrough()],
63
184
  ["crm_search_sale_products", "只读查询 CRM 上架产品列表;固定调用 /crmProduct/querySaleProductPageList,支持负责人、创建人的用户 ID 或唯一昵称筛选。", z.object({ page: z.number().optional(), limit: z.number().optional(), keyword: z.string().optional(), categoryId: idSchema.optional(), ...crmUserSearchShape }).passthrough()],
64
185
  ["crm_get_product", "只读查询 CRM 产品详情;固定调用 /crmProduct/queryById/{productId}。", z.object({ productId: idSchema.optional(), id: idSchema.optional() }).passthrough()],
@@ -123,8 +244,8 @@ const toolDefinitions = [
123
244
  ["crm_write_jxc", "执行 JXC 域内固定路径写入;targetPath 必须以 /jxc 开头,或传 module+operation 组合,必须 confirm=true 才会写入。", z.object({ targetPath: z.string().optional(), path: z.string().optional(), endpoint: z.string().optional(), module: z.string().optional(), operation: z.string().optional(), confirm: z.boolean().optional() }).passthrough()],
124
245
  ["crm_create_jxc_record", "新增 JXC 产品、供应商或仓库主数据;固定调用对应 /add,必须 confirm=true。", z.object({ module: moduleSchema, confirm: z.boolean().optional() }).passthrough()],
125
246
  ["crm_update_jxc_record", "编辑 JXC 产品、供应商或仓库主数据;默认调用对应 /update,可传 useInformationUpdate=true 调用 /updateInformation,必须 confirm=true。", z.object({ module: moduleSchema, id: idSchema.optional(), confirm: z.boolean().optional(), useInformationUpdate: z.boolean().optional() }).passthrough()],
126
- ["crm_list_calendar_events", "查询 OA 日历日程列表;固定调用 /oaEvent/queryList。startTime/endTime 使用 72CRM 接口要求的毫秒时间戳。", z.object({ startTime: z.union([z.string(), z.number()]), endTime: z.union([z.string(), z.number()]), typeIds: z.array(z.union([z.string(), z.number()])).optional(), userId: z.union([z.string(), z.number()]).optional() }).passthrough()],
127
- ["crm_get_calendar_event", "查询 OA 日历单条日程详情;固定调用 /oaEvent/queryById。", z.object({ eventId: z.union([z.string(), z.number()]).optional(), id: z.union([z.string(), z.number()]).optional(), startTime: z.union([z.string(), z.number()]).optional(), endTime: z.union([z.string(), z.number()]).optional() }).passthrough()],
247
+ ["crm_list_calendar_events", "查询 OA 日历日程列表;固定调用 /oaEvent/queryList。startTime/endTime 使用 72CRM 接口要求的毫秒时间戳。", z.object({ startTime: z.union([z.string(), z.number()]), endTime: z.union([z.string(), z.number()]), typeIds: z.array(idSchema).optional(), userId: idSchema.optional() }).passthrough()],
248
+ ["crm_get_calendar_event", "查询 OA 日历单条日程详情;固定调用 /oaEvent/queryById。", z.object({ eventId: idSchema.optional(), id: idSchema.optional(), startTime: z.union([z.string(), z.number()]).optional(), endTime: z.union([z.string(), z.number()]).optional() }).passthrough()],
128
249
  ["crm_list_oa_announcements", "查询 OA 公告列表;固定调用 /oaAnnouncement/queryList。", z.object({ page: z.number().optional(), limit: z.number().optional(), type: z.union([z.string(), z.number()]).optional() }).passthrough()],
129
250
  ["crm_get_oa_announcement", "查询 OA 公告详情;固定调用 /oaAnnouncement/queryById/{announcementId}。", z.object({ announcementId: idSchema.optional(), id: idSchema.optional() }).passthrough()],
130
251
  ["crm_create_oa_announcement", "新增 OA 公告;固定调用 /oaAnnouncement/addAnnouncement,必须 confirm=true。", z.object({ confirm: z.boolean().optional() }).passthrough()],
@@ -149,11 +270,11 @@ const toolDefinitions = [
149
270
  ["crm_list_oa_examine_record_logs", "只读查询 OA 审批流转日志;固定调用 /examineRecord/queryExamineRecordLog。", z.object({ recordId: idSchema.optional(), examineRecordId: idSchema.optional(), id: idSchema.optional() }).passthrough()],
150
271
  ["crm_list_oa_examine_flow_records", "只读查询 OA 审批流程节点记录;固定调用 /examineSuperExamine/examine/record/list。", z.object({ recordId: idSchema.optional(), examineRecordId: idSchema.optional(), id: idSchema.optional() }).passthrough()],
151
272
  ["crm_list_oa_tasks", "查询 OA 任务列表;固定调用 /oaTask/queryTaskList。", z.object({ page: z.number().optional(), limit: z.number().optional(), type: z.number().optional(), status: z.number().optional(), priority: z.number().optional() }).passthrough()],
152
- ["crm_list_related_oa_tasks", "按关联业务查询 OA 任务;固定调用 /oaTask/queryTypeTaskList。", z.object({ type: z.union([z.string(), z.number()]), typeId: z.union([z.string(), z.number()]) }).passthrough()],
273
+ ["crm_list_related_oa_tasks", "按关联业务查询 OA 任务;固定调用 /oaTask/queryTypeTaskList。", z.object({ type: z.union([z.string(), z.number()]), typeId: idSchema }).passthrough()],
153
274
  ["crm_preview_write", "写入前预览和校验,不调用 72CRM 写接口。", passthroughSchema],
154
- ["crm_create_record", "新增线索、客户、联系人、产品、商机;系统字段放 entity,自定义字段放 field,不支持把 customerName 放在最外层;必须 confirm=true 才会写入。", crmCreateRecordSchema],
275
+ ["crm_create_record", "新增线索、客户、联系人、产品、商机;系统字段放 entity,自定义字段放 field,不支持把 customerName 放在最外层。新增商机必须先用 crm_get_business_status_groups 获取合法的 entity.typeId/statusId,并提供 businessName/customerId;必须 confirm=true 才会写入。", crmCreateRecordSchema],
155
276
  ["crm_update_customer", "按客户 ID 或手机号/客户名唯一匹配后更新客户字段;会自动补全字段结构并写后校验,必须 confirm=true 才会写入。", z.object({ confirm: z.boolean().optional() }).passthrough()],
156
- ["crm_update_record_fields", "更新线索、客户、联系人、产品、商机基础字段;必须 confirm=true 才会写入。", z.object({ module: moduleSchema, id: z.union([z.string(), z.number()]), confirm: z.boolean().optional() }).passthrough()],
277
+ ["crm_update_record_fields", "更新线索、客户、联系人、产品、商机基础字段;必须 confirm=true 才会写入。", z.object({ module: moduleSchema, id: idSchema, confirm: z.boolean().optional() }).passthrough()],
157
278
  ["crm_create_product", "新增 CRM 产品;固定调用 /crmProduct/add,可直接传 72CRM payload 或 entity/field,必须 confirm=true。", z.object({ confirm: z.boolean().optional() }).passthrough()],
158
279
  ["crm_update_product", "编辑 CRM 产品;固定调用 /crmProduct/update,必须传 productId/id,必须 confirm=true。", z.object({ productId: idSchema.optional(), id: idSchema.optional(), confirm: z.boolean().optional() }).passthrough()],
159
280
  ["crm_update_product_fields", "更新 CRM 产品基础字段;固定调用 /crmProduct/updateInformation,会自动补字段结构,必须 confirm=true。", z.object({ productId: idSchema.optional(), id: idSchema.optional(), confirm: z.boolean().optional() }).passthrough()],
@@ -171,7 +292,7 @@ const toolDefinitions = [
171
292
  ["crm_create_quotation", "新增报价单;固定调用 /crmQuotation/add,可直接传 72CRM payload,或传 fields/updates 自动组装 entity/field/product/examineFlowData,必须 confirm=true。", z.object({ confirm: z.boolean().optional(), draft: z.boolean().optional() }).passthrough()],
172
293
  ["crm_update_quotation", "编辑报价单;固定调用 /crmQuotation/update,必须传 quotationId/id,可直接传 72CRM payload,或传 fields/updates 自动组装,必须 confirm=true。", z.object({ quotationId: idSchema.optional(), id: idSchema.optional(), confirm: z.boolean().optional(), draft: z.boolean().optional() }).passthrough()],
173
294
  ["crm_create_calendar_event", "新增 OA 日历日程;支持 72CRM SetEventBO 结构或顶层便捷字段,必须 confirm=true 才会写入。", z.object({ confirm: z.boolean().optional() }).passthrough()],
174
- ["crm_update_calendar_event", "修改 OA 日历日程;支持 72CRM SetEventBO 结构或顶层便捷字段,必须 confirm=true 才会写入。", z.object({ eventId: z.union([z.string(), z.number()]).optional(), id: z.union([z.string(), z.number()]).optional(), confirm: z.boolean().optional() }).passthrough()],
295
+ ["crm_update_calendar_event", "修改 OA 日历日程;支持 72CRM SetEventBO 结构或顶层便捷字段,必须 confirm=true 才会写入。", z.object({ eventId: idSchema.optional(), id: idSchema.optional(), confirm: z.boolean().optional() }).passthrough()],
175
296
  ["crm_create_oa_log", "新增 OA 日志;固定调用 /oaLog/addOrUpdate,可直接传 payload,或传 categoryId+fields 自动组装日志模板字段,必须 confirm=true。", z.object({ categoryId: idSchema.optional(), confirm: z.boolean().optional() }).passthrough()],
176
297
  ["crm_update_oa_log", "编辑 OA 日志;固定调用 /oaLog/addOrUpdate,必须传 logId/id,可直接传 payload 或自动组装字段,必须 confirm=true。", z.object({ logId: idSchema.optional(), id: idSchema.optional(), categoryId: idSchema.optional(), confirm: z.boolean().optional() }).passthrough()],
177
298
  ["crm_delete_oa_log", "删除 OA 日志;固定调用 /oaLog/deleteById,logId 走查询参数,必须 confirm=true。", z.object({ logId: idSchema.optional(), id: idSchema.optional(), confirm: z.boolean().optional() }).passthrough()],
@@ -181,9 +302,9 @@ const toolDefinitions = [
181
302
  ["crm_audit_oa_examine", "处理单条 OA 审批;固定调用 /examineRecord/auditExamine,必须传 recordId/examineRecordId/id 且 confirm=true。", z.object({ recordId: idSchema.optional(), examineRecordId: idSchema.optional(), id: idSchema.optional(), confirm: z.boolean().optional() }).passthrough()],
182
303
  ["crm_batch_audit_oa_examine", "批量处理 OA 审批;固定调用 /examineRecord/batchAuditExamine,必须传 recordIds/examineRecordIds/ids 且 confirm=true。", z.object({ recordIds: z.array(idSchema).optional(), examineRecordIds: z.array(idSchema).optional(), ids: z.array(idSchema).optional(), confirm: z.boolean().optional() }).passthrough()],
183
304
  ["crm_create_oa_task", "新增 OA 主任务;固定调用 /workTask/saveWorkTask,支持 WorkTask 结构或顶层便捷字段,必须 confirm=true 才会写入。", z.object({ confirm: z.boolean().optional() }).passthrough()],
184
- ["crm_update_oa_task_fields", "更新 OA 主任务字段;按字段映射到 /workTask 下的标题、描述、负责人、参与人、时间、标签、优先级、状态接口,必须 confirm=true 才会写入。", z.object({ taskId: z.union([z.string(), z.number()]).optional(), id: z.union([z.string(), z.number()]).optional(), confirm: z.boolean().optional() }).passthrough()],
185
- ["crm_add_followup", "新增跟进记录;必须 confirm=true 才会写入。", z.object({ confirm: z.boolean().optional() }).passthrough()],
186
- ["crm_update_followup", "更新跟进记录;必须 confirm=true 才会写入。", z.object({ id: z.union([z.string(), z.number()]).optional(), confirm: z.boolean().optional() }).passthrough()],
305
+ ["crm_update_oa_task_fields", "更新 OA 主任务字段;按字段映射到 /workTask 下的标题、描述、负责人、参与人、时间、标签、优先级、状态接口,必须 confirm=true 才会写入。", z.object({ taskId: idSchema.optional(), id: idSchema.optional(), confirm: z.boolean().optional() }).passthrough()],
306
+ ["crm_add_followup", "新增跟进记录;entity 必须提供 content、category、activityType 和 activityTypeId,MCP 会先回读关联记录再写入;必须 confirm=true 才会写入。", crmAddFollowupSchema],
307
+ ["crm_update_followup", "更新跟进记录;完整更新必须提供 entity,字段更新使用 useInformationUpdate=true 和 list;必须 confirm=true 才会写入。", crmUpdateFollowupSchema],
187
308
  ["workorder_search_records", "查询工单、工单池或线下工单;scope 可选 workorder、pool、offline。", z.object({ scope: z.enum(["workorder", "pool", "offline"]).optional(), page: z.number().optional(), limit: z.number().optional() }).passthrough()],
188
309
  ["workorder_get_record", "查询普通工单或线下工单详情及详情字段;不执行状态流转。", z.object({ scope: z.enum(["workorder", "offline"]).optional(), id: idSchema }).passthrough()],
189
310
  ["workorder_get_schema", "查询普通工单或线下工单字段结构,可传 id 获取编辑字段。", z.object({ scope: z.enum(["workorder", "offline"]).optional(), id: idSchema.optional() }).passthrough()],
@@ -195,9 +316,13 @@ export function createWukongMcpServer(config = {}) {
195
316
  const { oauth, ...crmConfig } = config;
196
317
  const server = new McpServer({
197
318
  name: "wukong-mcp",
198
- version: "0.2.5"
319
+ version: "0.2.6"
199
320
  }, {
200
321
  instructions: "先使用只读工具确认模块、记录和字段,再执行写入。所有写入必须显式传 confirm=true;不要尝试任意 URL 请求。"
322
+ + "所有 ID 必须使用数字字符串,禁止把 Snowflake ID 作为 JSON number 传入。"
323
+ + "写工具正常返回且 writeOutcome=confirmed 即表示成功;data=null 只表示旧 CRM 未返回业务数据。"
324
+ + "工具返回错误时不得声称原操作成功,也不得未经用户明确同意改用 OA 任务等其他模块替代。"
325
+ + "写入错误若标记 writeOutcome=unknown,必须先回读原目标确认是否落库,禁止直接重放。"
201
326
  });
202
327
  const sharedClient = new CrmClient(crmConfig);
203
328
  const handlers = {
@@ -209,21 +334,23 @@ export function createWukongMcpServer(config = {}) {
209
334
  name,
210
335
  description,
211
336
  schema,
212
- access: toolAccess(name, description),
213
- destructive: isDestructiveTool(name)
337
+ access: toolAccess(name),
338
+ destructive: isDestructiveTool(name),
339
+ discovery: getToolDiscovery(name, description)
214
340
  })),
215
341
  ...nocodeToolDefinitions.map((definition) => ({
216
342
  ...definition,
217
- destructive: definition.destructive ?? false
343
+ destructive: definition.destructive ?? false,
344
+ discovery: getToolDiscovery(definition.name, definition.description)
218
345
  }))
219
346
  ];
220
- for (const { name, description, schema, access, destructive } of registeredTools) {
347
+ for (const { name, description, schema, access, destructive, discovery } of registeredTools) {
221
348
  const requiredScopes = access === "write"
222
349
  ? [CRM_READ_SCOPE, CRM_WRITE_SCOPE]
223
350
  : [CRM_READ_SCOPE];
224
351
  const securitySchemes = [{ type: "oauth2", scopes: requiredScopes }];
225
352
  server.registerTool(name, {
226
- title: name,
353
+ title: discovery.title,
227
354
  description,
228
355
  inputSchema: schema,
229
356
  outputSchema: toolOutputSchema,
@@ -234,8 +361,9 @@ export function createWukongMcpServer(config = {}) {
234
361
  ...(access === "read" ? { idempotentHint: true } : {})
235
362
  },
236
363
  _meta: {
237
- // ChatGPT 在扫描工具时会读取这个兼容字段。
238
- securitySchemes
364
+ // securitySchemes 供通用 MCP 客户端鉴权;命名空间字段供宿主做确定性发现,不进入业务参数。
365
+ securitySchemes,
366
+ [TOOL_DISCOVERY_META_KEY]: discovery.meta
239
367
  }
240
368
  }, async (args) => {
241
369
  const missingScopes = oauth
@@ -249,8 +377,9 @@ export function createWukongMcpServer(config = {}) {
249
377
  if (!handler) {
250
378
  throw new Error(`未找到工具实现:${name}`);
251
379
  }
252
- const result = await handler(args ?? {});
253
- return toToolResult(result);
380
+ const normalizedArgs = normalizeToolArguments(args ?? {});
381
+ const result = await handler(normalizedArgs);
382
+ return toToolResult(access === "write" ? withSuccessfulWriteOutcome(result) : result);
254
383
  }
255
384
  catch (error) {
256
385
  return {
@@ -278,14 +407,59 @@ function toToolResult(result) {
278
407
  ]
279
408
  };
280
409
  }
281
- function toolAccess(name, description) {
282
- if (name === "crm_preview_write") {
283
- return "read";
410
+ function withSuccessfulWriteOutcome(result) {
411
+ if (isRecord(result) && result.preview === true) {
412
+ return result;
284
413
  }
285
- return description.includes("confirm=true") ? "write" : "read";
414
+ const details = isRecord(result) ? result : { data: result ?? null };
415
+ return {
416
+ ...details,
417
+ success: true,
418
+ requestAccepted: true,
419
+ crmCode: 0,
420
+ writeOutcome: "confirmed"
421
+ };
422
+ }
423
+ function normalizeToolArguments(value, path = "arguments") {
424
+ if (Array.isArray(value)) {
425
+ return value.map((item, index) => normalizeToolArguments(item, `${path}[${index}]`));
426
+ }
427
+ if (!isRecord(value)) {
428
+ return value;
429
+ }
430
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => {
431
+ const itemPath = `${path}.${key}`;
432
+ return [key, isIdFieldName(key)
433
+ ? normalizeIdFieldValue(item, itemPath)
434
+ : normalizeToolArguments(item, itemPath)];
435
+ }));
436
+ }
437
+ function normalizeIdFieldValue(value, path) {
438
+ if (Array.isArray(value)) {
439
+ return value.map((item, index) => normalizeIdFieldValue(item, `${path}[${index}]`));
440
+ }
441
+ if (isRecord(value)) {
442
+ return normalizeToolArguments(value, path);
443
+ }
444
+ if (typeof value !== "number") {
445
+ return value;
446
+ }
447
+ if (!Number.isSafeInteger(value)) {
448
+ throw new Error(`${path} 使用了不安全的 JSON number,原始 ID 可能已丢失精度;请改用数字字符串。`);
449
+ }
450
+ return String(value);
451
+ }
452
+ function isIdFieldName(key) {
453
+ return key === "id" || key === "ids" || /Ids?$/.test(key) || /_ids?$/i.test(key);
454
+ }
455
+ function isRecord(value) {
456
+ return value !== null && typeof value === "object" && !Array.isArray(value);
457
+ }
458
+ function toolAccess(name) {
459
+ return Object.hasOwn(WRITE_TOOL_SAFETY, name) ? "write" : "read";
286
460
  }
287
461
  function isDestructiveTool(name) {
288
- return /^crm_(?:update|delete|transfer|toggle|audit|batch_audit|write|save)_/.test(name);
462
+ return WRITE_TOOL_SAFETY[name]?.destructive ?? false;
289
463
  }
290
464
  function insufficientScopeResult(requiredScopes, missingScopes, resourceMetadataUrl) {
291
465
  const requiredScopeText = requiredScopes.join(" ");
@@ -0,0 +1,18 @@
1
+ export declare const TOOL_DISCOVERY_META_KEY = "com.wukongcrm/tool-discovery";
2
+ export interface ToolDiscoveryMeta {
3
+ version: 1;
4
+ domain: string;
5
+ module: string;
6
+ entities: string[];
7
+ actions: string[];
8
+ capability: string;
9
+ aliases: string[];
10
+ excludes?: string[];
11
+ requires?: string[];
12
+ verifiesWith?: string[];
13
+ }
14
+ export interface ToolDiscoveryDefinition {
15
+ title: string;
16
+ meta: ToolDiscoveryMeta;
17
+ }
18
+ export declare function getToolDiscovery(name: string, _description: string): ToolDiscoveryDefinition;
@@ -0,0 +1,186 @@
1
+ import { CRM_MODULES } from "./modules.js";
2
+ export const TOOL_DISCOVERY_META_KEY = "com.wukongcrm/tool-discovery";
3
+ const CRM_ENTITIES = CRM_MODULES.flatMap((definition) => [definition.key, definition.name]);
4
+ // 这批工具容易因“详情、动态、跟进、操作记录”等相邻概念误选,元数据按真实接口语义显式区分。
5
+ const CORE_DISCOVERY = {
6
+ crm_search_records: {
7
+ title: "搜索 CRM 记录",
8
+ domain: "crm",
9
+ module: "record",
10
+ entities: CRM_ENTITIES,
11
+ actions: ["search", "list", "查询", "搜索"],
12
+ capability: "crm.record.search",
13
+ aliases: ["查询客户", "搜索联系人", "商机列表", "按负责人查询", "查询跟进记录"],
14
+ excludes: ["单条详情", "记录动态", "操作记录"]
15
+ },
16
+ crm_get_record: {
17
+ title: "查询 CRM 单条记录",
18
+ domain: "crm",
19
+ module: "record",
20
+ entities: CRM_ENTITIES,
21
+ actions: ["get", "read", "查询", "查看"],
22
+ capability: "crm.record.detail.read",
23
+ aliases: ["记录详情", "客户详情", "联系人详情", "商机详情", "按 ID 查询记录", "确认目标记录"],
24
+ excludes: ["详情页字段", "记录动态", "跟进记录列表", "操作记录", "关联资料"]
25
+ },
26
+ crm_get_record_information: {
27
+ title: "查询 CRM 详情页字段",
28
+ domain: "crm",
29
+ module: "record-information",
30
+ entities: CRM_ENTITIES,
31
+ actions: ["get", "read", "查询", "查看"],
32
+ capability: "crm.record.field-information.read",
33
+ aliases: ["详情页信息", "字段中文名和值", "基本信息字段", "详情字段展示"],
34
+ excludes: ["完整记录对象", "记录动态", "跟进记录列表", "操作记录"]
35
+ },
36
+ crm_get_related_records: {
37
+ title: "查询 CRM 关联资料",
38
+ domain: "crm",
39
+ module: "record-relation",
40
+ entities: CRM_ENTITIES,
41
+ actions: ["get", "read", "查询"],
42
+ capability: "crm.record.relation.read",
43
+ aliases: ["关联资料", "关联数据", "相关产品", "相关联系人", "回款计划"],
44
+ excludes: ["记录动态", "跟进记录", "操作记录"]
45
+ },
46
+ crm_get_record_timeline: {
47
+ title: "查询 CRM 记录动态",
48
+ domain: "crm",
49
+ module: "record-dynamics",
50
+ entities: CRM_ENTITIES,
51
+ actions: ["get", "read", "查询"],
52
+ capability: "crm.record.dynamics.read",
53
+ aliases: ["记录动态", "动态时间线", "创建记录", "字段变化", "阶段变化", "系统动态"],
54
+ excludes: ["跟进记录列表", "操作审计记录", "市场活动", "关联资料"]
55
+ },
56
+ crm_get_action_records: {
57
+ title: "查询 CRM 操作记录",
58
+ domain: "crm",
59
+ module: "record-audit",
60
+ entities: CRM_ENTITIES,
61
+ actions: ["get", "read", "查询"],
62
+ capability: "crm.record.audit-log.read",
63
+ aliases: ["操作记录", "变更日志", "审计记录", "操作审计"],
64
+ excludes: ["记录动态", "跟进记录列表"]
65
+ },
66
+ crm_get_business_status_groups: {
67
+ title: "查询商机状态组",
68
+ domain: "crm",
69
+ module: "business",
70
+ entities: ["business", "商机", "商机状态组", "商机阶段"],
71
+ actions: ["get", "read", "查询"],
72
+ capability: "crm.business.status.read",
73
+ aliases: ["商机状态组", "商机阶段", "创建商机前置", "typeId", "statusId"],
74
+ excludes: ["创建商机记录"]
75
+ },
76
+ crm_create_record: {
77
+ title: "新建 CRM 记录",
78
+ domain: "crm",
79
+ module: "record",
80
+ entities: ["leads", "线索", "customer", "客户", "contacts", "联系人", "product", "产品", "business", "商机"],
81
+ actions: ["create", "add", "创建", "新增"],
82
+ capability: "crm.record.create",
83
+ aliases: ["创建客户", "新增联系人", "新建产品", "创建商机", "新增线索"],
84
+ // 聚合创建只在目标实体为商机时需要该前置能力;消费者应结合依赖工具的 entities 判断是否联动发现。
85
+ requires: ["crm.business.status.read"],
86
+ verifiesWith: ["crm.record.detail.read"]
87
+ },
88
+ crm_add_followup: {
89
+ title: "新增 CRM 跟进记录",
90
+ domain: "crm",
91
+ module: "followup",
92
+ entities: ["activity", "跟进记录", "线索", "客户", "联系人", "产品", "商机", "合同", "回款", "回款计划", "回访", "报价单"],
93
+ actions: ["create", "add", "创建", "新增"],
94
+ capability: "crm.followup.create",
95
+ aliases: ["创建跟进记录", "新增跟进", "写跟进内容", "记录拜访结果"],
96
+ excludes: ["查询跟进记录", "记录动态"],
97
+ verifiesWith: ["crm.record.detail.read"]
98
+ },
99
+ nocode_search_records: {
100
+ title: "搜索无代码记录",
101
+ domain: "nocode",
102
+ module: "record",
103
+ entities: ["nocode-record", "无代码记录", "应用记录", "模块记录"],
104
+ actions: ["search", "list", "查询", "搜索"],
105
+ capability: "nocode.record.search",
106
+ aliases: ["查询无代码模块记录", "应用数据列表", "搜索模块数据"]
107
+ },
108
+ nocode_write_record: {
109
+ title: "写入无代码记录",
110
+ domain: "nocode",
111
+ module: "record",
112
+ entities: ["nocode-record", "无代码记录", "应用记录", "模块记录"],
113
+ actions: ["create", "update", "delete", "创建", "修改", "删除"],
114
+ capability: "nocode.record.write",
115
+ aliases: ["新增无代码记录", "修改应用数据", "删除模块记录"],
116
+ verifiesWith: ["nocode.record.search"]
117
+ }
118
+ };
119
+ const ACTION_BY_VERB = {
120
+ list: "list",
121
+ search: "search",
122
+ query: "query",
123
+ get: "get",
124
+ find: "search",
125
+ auth: "read",
126
+ validate: "validate",
127
+ preview: "preview",
128
+ calculate: "calculate",
129
+ check: "validate",
130
+ create: "create",
131
+ add: "create",
132
+ save: "create",
133
+ update: "update",
134
+ write: "write",
135
+ delete: "delete",
136
+ transfer: "transfer",
137
+ toggle: "update",
138
+ audit: "audit",
139
+ batch: "batch"
140
+ };
141
+ function inferDomainAndModule(name) {
142
+ if (name.startsWith("nocode_"))
143
+ return { domain: "nocode", module: "nocode" };
144
+ if (name.startsWith("workorder_"))
145
+ return { domain: "workorder", module: "workorder" };
146
+ if (name.startsWith("project_"))
147
+ return { domain: "project", module: "project" };
148
+ if (name.includes("_hrm_"))
149
+ return { domain: "hrm", module: "hrm" };
150
+ if (name.includes("_finance_"))
151
+ return { domain: "finance", module: "finance" };
152
+ if (name.includes("_jxc_"))
153
+ return { domain: "jxc", module: "jxc" };
154
+ if (name.includes("_oa_") || name.includes("_calendar_") || name.includes("_address_book_")) {
155
+ return { domain: "oa", module: "oa" };
156
+ }
157
+ return { domain: "crm", module: "crm" };
158
+ }
159
+ function inferAction(name) {
160
+ const prefix = name.startsWith("workorder_") ? "workorder_"
161
+ : name.startsWith("project_") ? "project_"
162
+ : name.startsWith("nocode_") ? "nocode_" : "crm_";
163
+ const verb = name.slice(prefix.length).split("_")[0] ?? "read";
164
+ return ACTION_BY_VERB[verb] ?? verb;
165
+ }
166
+ export function getToolDiscovery(name, _description) {
167
+ const inferred = inferDomainAndModule(name);
168
+ const override = CORE_DISCOVERY[name];
169
+ const action = inferAction(name);
170
+ return {
171
+ title: override?.title ?? name,
172
+ meta: {
173
+ version: 1,
174
+ domain: override?.domain ?? inferred.domain,
175
+ module: override?.module ?? inferred.module,
176
+ entities: override?.entities ?? [inferred.module],
177
+ actions: override?.actions ?? [action],
178
+ capability: override?.capability ?? `wukong.${name.replaceAll("_", ".")}`,
179
+ // 长尾工具先依靠标准 description/Schema 降级检索;未经源码确认的“同义词”不能伪装成 aliases。
180
+ aliases: override?.aliases ?? [],
181
+ ...(override?.excludes ? { excludes: override.excludes } : {}),
182
+ ...(override?.requires ? { requires: override.requires } : {}),
183
+ ...(override?.verifiesWith ? { verifiesWith: override.verifiesWith } : {})
184
+ }
185
+ };
186
+ }