@wukongcrm/mcp-server 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -147,7 +147,7 @@ npm pack
147
147
  生成文件示例:
148
148
 
149
149
  ```text
150
- wukongcrm-mcp-server-0.2.0.tgz
150
+ wukongcrm-mcp-server-0.2.2.tgz
151
151
  ```
152
152
 
153
153
  这个包只包含运行所需的 `dist`、`README.md` 和 `package.json`,不会携带源码、测试文件或用户 API Key。
@@ -155,7 +155,7 @@ wukongcrm-mcp-server-0.2.0.tgz
155
155
  使用者拿到 `.tgz` 后安装:
156
156
 
157
157
  ```powershell
158
- npm install -g .\wukongcrm-mcp-server-0.2.0.tgz
158
+ npm install -g .\wukongcrm-mcp-server-0.2.2.tgz
159
159
  ```
160
160
 
161
161
  安装后可以直接用命令启动:
@@ -198,6 +198,12 @@ wukong-mcp
198
198
  - 打印与消息:`nocode_query_printing`、`nocode_write_printing`、`nocode_query_messages`、`nocode_write_message`
199
199
  - 开放能力与应用市场:`nocode_query_integrations`、`nocode_write_integration`、`nocode_query_marketplace`、`nocode_write_marketplace`
200
200
 
201
+ 常见保存动作必须使用旧前端对应的聚合 BO,而不是把单个字段或单个值直接作为 `payload`:
202
+
203
+ - 字段保存:`ModuleFieldSaveBO`,至少包含 `moduleId`、实际 `version`、`mainFieldId`/`tempMainFieldId` 和非空 `moduleFieldList`。
204
+ - 记录保存:`ModuleFieldDataSaveBO`,至少包含 `moduleId`、实际 `version` 和非空 `fieldDataList`;字段值使用 `fieldId`、`fieldName`、`name`、`type`、`value`,修改时增加 `dataId`。
205
+ - 新建模块的实际版本通常从 `0` 开始,应先用 `nocode_get_module` 读取并显式传入,不能根据已发布老模块猜测为 `1`。
206
+
201
207
  文件流仍有意不经 MCP 代理,因此应用导入/导出、Excel 导入/导出、跟进导出、打印 PDF/Word 下载不在工具中;打印内容生成和预览 JSON 接口仍可使用。内部测试接口 `/moduleMQ/send` 也不会暴露。
202
208
 
203
209
  ### CRM、OA、HRM、财务、进销存与工单工具
package/dist/client.d.ts CHANGED
@@ -19,6 +19,7 @@ export declare class CrmClient {
19
19
  private readonly apiKey;
20
20
  private readonly fetchImpl;
21
21
  private adminToken;
22
+ private loginPromise;
22
23
  constructor(config?: CrmClientConfig);
23
24
  authStatus(): Promise<AuthStatusResult>;
24
25
  call<T = unknown>(method: "GET" | "POST", path: string, body?: unknown, query?: Record<string, unknown>): Promise<T>;
package/dist/client.js CHANGED
@@ -5,6 +5,7 @@ export class CrmClient {
5
5
  apiKey;
6
6
  fetchImpl;
7
7
  adminToken = "";
8
+ loginPromise;
8
9
  constructor(config = {}) {
9
10
  this.baseUrl = normalizeBaseUrl(config.baseUrl ?? process.env.CRM_BASE_URL ?? "https://www.72crm.com/api-11/");
10
11
  this.apiKey = config.apiKey ?? process.env.CRM_API_KEY ?? "";
@@ -51,7 +52,14 @@ export class CrmClient {
51
52
  if (!force && this.adminToken.trim()) {
52
53
  return;
53
54
  }
54
- this.adminToken = await this.loginWithApiKey();
55
+ // Schema 聚合会并发请求多个只读接口。首次调用时必须把 API Key 登录合并成一次,
56
+ // 否则旧 CRM 会在短时间内收到多次登录并让部分请求鉴权失败。
57
+ if (!this.loginPromise) {
58
+ this.loginPromise = this.loginWithApiKey().finally(() => {
59
+ this.loginPromise = undefined;
60
+ });
61
+ }
62
+ this.adminToken = await this.loginPromise;
55
63
  }
56
64
  async loginWithApiKey() {
57
65
  try {
package/dist/nocode.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- import { type CrmClientConfig } from "./client.js";
2
+ import { CrmClient, type CrmClientConfig } from "./client.js";
3
3
  export type NocodeToolAccess = "read" | "write";
4
4
  export interface NocodeToolDefinition {
5
5
  name: string;
@@ -12,5 +12,5 @@ type AnyArgs = Record<string, any>;
12
12
  type Handler = (args: AnyArgs) => Promise<any> | any;
13
13
  export type NocodeToolHandlers = Record<string, Handler>;
14
14
  export declare const nocodeToolDefinitions: NocodeToolDefinition[];
15
- export declare function createNocodeToolHandlers(config?: CrmClientConfig): NocodeToolHandlers;
15
+ export declare function createNocodeToolHandlers(config?: CrmClientConfig, sharedClient?: CrmClient): NocodeToolHandlers;
16
16
  export {};
package/dist/nocode.js CHANGED
@@ -1,6 +1,9 @@
1
1
  import { z } from "zod";
2
2
  import { CrmClient } from "./client.js";
3
- const idSchema = z.union([z.string(), z.number()]);
3
+ const idSchema = z.union([
4
+ z.string().regex(/^\d+$/, "ID 必须是数字字符串。"),
5
+ z.number().int().safe("超过 JavaScript 安全整数范围的 ID 必须以字符串传入。")
6
+ ]);
4
7
  const payloadSchema = z.record(z.unknown());
5
8
  const confirmSchema = z.boolean().optional();
6
9
  const versionSchema = z.number().int().nonnegative().optional();
@@ -62,7 +65,7 @@ export const nocodeToolDefinitions = [
62
65
  },
63
66
  {
64
67
  name: "nocode_get_module_schema",
65
- description: "一次获取无代码模块字段、表单布局、列表头、页面布局、选项、公式、标签和序列号规则;写入记录前应先调用;只读。",
68
+ description: "一次获取无代码模块字段、表单布局、列表头、页面布局、选项、公式、标签和序列号规则;写入记录前应先调用,并优先使用 nocode_get_module 返回的实际 version(新建模块通常为 0);雪花 moduleId 必须以字符串传入,避免 JavaScript 数字精度丢失;只读。",
66
69
  schema: z.object({ ...moduleContextShape, includeExtended: z.boolean().optional() }),
67
70
  access: "read"
68
71
  },
@@ -193,7 +196,7 @@ export const nocodeToolDefinitions = [
193
196
  },
194
197
  {
195
198
  name: "nocode_query_printing",
196
- description: "查询无代码打印模板字段、模板列表/详情和打印记录;只读。打印、预览和模板变更请使用 nocode_write_printing。",
199
+ description: "查询无代码打印模板字段、模板列表/详情和打印记录;只读。records 会先确认当前模块存在打印模板,规避旧后端在空模板集合上生成无效 SQL;打印、预览和模板变更请使用 nocode_write_printing。",
197
200
  schema: z.object({
198
201
  operation: z.enum(["fields", "templates", "template", "records", "record"]),
199
202
  ...extendedContextShape
@@ -243,7 +246,7 @@ export const nocodeToolDefinitions = [
243
246
  },
244
247
  {
245
248
  name: "nocode_write_module",
246
- description: "保存、启停、复制、发布、丢弃草稿、删除模块、保存页面布局或维护 BI Dashboard 模块;必须 confirm=true。",
249
+ description: "保存、启停、复制、发布、丢弃草稿、删除模块、保存页面布局或维护 BI Dashboard 模块;action=save 的 payload 使用旧 CRM ModuleSaveBO(常见普通模块包含 applicationId、name、moduleType=1、icon);必须 confirm=true。",
247
250
  schema: z.object({
248
251
  action: z.enum(["save", "status", "copy", "publish", "discardDraft", "delete", "layout", "deleteCancelledCompanyData", "dashboardSave", "dashboardRemove"]),
249
252
  moduleId: idSchema.optional(),
@@ -257,14 +260,14 @@ export const nocodeToolDefinitions = [
257
260
  },
258
261
  {
259
262
  name: "nocode_write_field",
260
- description: "保存字段,或修改普通/固定模块列表字段排序和样式;必须 confirm=true,否则只返回固定接口和请求预览。",
263
+ description: "保存字段,或修改普通/固定模块列表字段排序和样式;action=save 的 payload 必须是聚合 ModuleFieldSaveBO,包含 moduleId、实际 version、mainFieldId 或 tempMainFieldId、moduleFieldList,不能直接传单个字段;必须 confirm=true,否则只返回固定接口和请求预览。",
261
264
  schema: z.object({ action: z.enum(["save", "sort", "style", "fixedSort", "fixedStyle"]), ...payloadWriteShape }),
262
265
  access: "write",
263
266
  destructive: true
264
267
  },
265
268
  {
266
269
  name: "nocode_write_record",
267
- description: "新增/修改、删除、转移负责人或设置分组;payload 使用对应旧 CRM BO;必须 confirm=true,否则只返回预览。",
270
+ description: "新增/修改、删除、转移负责人或设置分组;action=save 的 payload 必须是 ModuleFieldDataSaveBO,包含 moduleId、实际 version、fieldDataList;每个字段值应包含 fieldId、fieldName、name、type、value,修改时再带 dataId;必须 confirm=true,否则只返回预览。",
268
271
  schema: z.object({ action: z.enum(["save", "delete", "transfer", "setCategory"]), ...payloadWriteShape }),
269
272
  access: "write",
270
273
  destructive: true
@@ -395,8 +398,8 @@ export const nocodeToolDefinitions = [
395
398
  destructive: true
396
399
  }
397
400
  ];
398
- export function createNocodeToolHandlers(config = {}) {
399
- const client = new CrmClient(config);
401
+ export function createNocodeToolHandlers(config = {}, sharedClient) {
402
+ const client = sharedClient ?? new CrmClient(config);
400
403
  return {
401
404
  nocode_list_applications: async () => readonlyResult("/moduleMetadata/customAppList", await client.post("/moduleMetadata/customAppList")),
402
405
  nocode_get_application: async (args) => {
@@ -430,10 +433,24 @@ export function createNocodeToolHandlers(config = {}) {
430
433
  const moduleId = requiredId(args.moduleId, "moduleId");
431
434
  const version = requiredVersion(args.version);
432
435
  const query = cleanObject({ moduleId: numericValue(moduleId), version, categoryId: args.categoryId });
433
- const [fields, form, listHead, layout, unionModules] = await Promise.all([
436
+ // 旧后端会直接拆箱 filterHidden;显式传 false 与无代码前端的字段配置请求保持一致,避免空值触发 500。
437
+ const formQuery = { ...query, filterHidden: false };
438
+ const [fields, form, listHeadOutcome, layout, unionModules] = await Promise.all([
434
439
  client.post("/moduleField/queryList", query),
435
- client.post("/moduleField/formList", query),
436
- client.post("/moduleField/queryListHead", query),
440
+ client.post("/moduleField/formList", formQuery),
441
+ client.post("/moduleField/queryListHead", query)
442
+ .then((data) => ({ data, warning: undefined }))
443
+ .catch((error) => {
444
+ // 旧端的列表头服务只读取“已发布激活”的模块。新建草稿在字段已经存在时仍会返回 4303,
445
+ // 但字段列表和表单结构本身可正常读取;不要让该可预期边界拖垮整个聚合 Schema。
446
+ if (isModuleNotFoundError(error)) {
447
+ return {
448
+ data: [],
449
+ warning: "当前模块尚未发布,旧 CRM 不提供列表头;fields/form 仍是有效的草稿 Schema。"
450
+ };
451
+ }
452
+ throw error;
453
+ }),
437
454
  client.post(`/moduleM/pageLayout/${moduleId}/${version}`),
438
455
  client.post(`/moduleM/unionModules/${moduleId}/${version}`)
439
456
  ]);
@@ -451,9 +468,10 @@ export function createNocodeToolHandlers(config = {}) {
451
468
  version,
452
469
  fields,
453
470
  form,
454
- listHead,
471
+ listHead: listHeadOutcome.data,
455
472
  layout,
456
473
  unionModules,
474
+ ...(listHeadOutcome.warning ? { warnings: [listHeadOutcome.warning] } : {}),
457
475
  ...extended
458
476
  };
459
477
  },
@@ -511,20 +529,20 @@ export function createNocodeToolHandlers(config = {}) {
511
529
  nocode_query_workflow: async (args) => executeNocodeRead(client, queryWorkflowRoute(args)),
512
530
  nocode_query_followups: async (args) => executeNocodeRead(client, queryFollowUpRoute(args)),
513
531
  nocode_query_configuration: async (args) => executeNocodeRead(client, queryConfigurationRoute(args)),
514
- nocode_query_printing: async (args) => executeNocodeRead(client, queryPrintingRoute(args)),
532
+ nocode_query_printing: async (args) => executePrintingRead(client, args),
515
533
  nocode_query_messages: async (args) => executeNocodeRead(client, queryMessageRoute(args)),
516
534
  nocode_query_integrations: async (args) => executeNocodeRead(client, queryIntegrationRoute(args)),
517
535
  nocode_query_marketplace: async (args) => executeNocodeRead(client, queryMarketplaceRoute(args)),
518
536
  nocode_write_application: async (args) => executeApplicationWrite(client, args),
519
537
  nocode_write_module: async (args) => executeModuleWrite(client, args),
520
- nocode_write_field: async (args) => executePayloadWrite(client, args, {
538
+ nocode_write_field: async (args) => executeFieldWrite(client, args, {
521
539
  save: "/moduleField/save",
522
540
  sort: "/moduleField/setFieldSort",
523
541
  style: "/moduleField/setFieldStyle",
524
542
  fixedSort: "/moduleField/fixed/setFieldSort",
525
543
  fixedStyle: "/moduleField/fixed/setFieldStyle"
526
544
  }),
527
- nocode_write_record: async (args) => executePayloadWrite(client, args, {
545
+ nocode_write_record: async (args) => executeRecordWrite(client, args, {
528
546
  save: "/moduleFieldData/save",
529
547
  delete: "/moduleFieldData/delete",
530
548
  transfer: "/moduleFieldData/transfer",
@@ -564,6 +582,41 @@ export function createNocodeToolHandlers(config = {}) {
564
582
  async function executeNocodeRead(client, route) {
565
583
  return readonlyResult(route.targetPath, await client.post(route.targetPath, route.body, route.query), cleanObject({ operation: route.operation, body: route.body, query: route.query }));
566
584
  }
585
+ async function executePrintingRead(client, args) {
586
+ const route = queryPrintingRoute(args);
587
+ if (route.operation !== "records") {
588
+ return executeNocodeRead(client, route);
589
+ }
590
+ const moduleId = requiredId(args.moduleId, "moduleId");
591
+ let version;
592
+ if (args.version !== undefined) {
593
+ version = requiredVersion(args.version);
594
+ }
595
+ else {
596
+ const moduleDetail = await client.post(`/moduleM/queryById/${moduleId}`, undefined, { isLatest: true });
597
+ version = requiredVersion(moduleDetail?.version);
598
+ }
599
+ // 旧后端 queryPrintRecord 会对空模板 ID 集合直接拼接 IN (),最终只返回笼统的
600
+ // “Network Busy”。先用模板列表判断空集,空时按真实业务语义返回无打印记录。
601
+ const templatePath = `/modulePrintTemplate/list/${moduleId}/${version}`;
602
+ const templatePage = await client.post(templatePath, { page: 1, limit: 1 });
603
+ const templateRows = listRows(templatePage);
604
+ if (templateRows?.length === 0) {
605
+ return readonlyResult(route.targetPath, [], {
606
+ operation: route.operation,
607
+ preflight: { targetPath: templatePath, version, emptyTemplates: true }
608
+ });
609
+ }
610
+ return executeNocodeRead(client, route);
611
+ }
612
+ function listRows(value) {
613
+ if (Array.isArray(value))
614
+ return value;
615
+ if (!value || typeof value !== "object")
616
+ return undefined;
617
+ const list = value.list;
618
+ return Array.isArray(list) ? list : undefined;
619
+ }
567
620
  function executeNocodeWrite(client, args, route) {
568
621
  return confirmedPost(client, args, route.operation, route.targetPath, route.body, route.query);
569
622
  }
@@ -874,11 +927,11 @@ async function executeApplicationWrite(client, args) {
874
927
  switch (action) {
875
928
  case "create":
876
929
  targetPath = "/moduleMetadata/save";
877
- body = safePayload(args.payload);
930
+ body = applicationPayload(args.payload, action);
878
931
  break;
879
932
  case "update":
880
933
  targetPath = "/moduleMetadata/update";
881
- body = safePayload(args.payload);
934
+ body = applicationPayload(args.payload, action);
882
935
  break;
883
936
  case "status":
884
937
  targetPath = `/moduleMetadata/updateStatus/${requiredId(args.applicationId, "applicationId")}`;
@@ -954,6 +1007,20 @@ async function executePayloadWrite(client, args, paths) {
954
1007
  }
955
1008
  return confirmedPost(client, args, action, targetPath, safePayload(args.payload));
956
1009
  }
1010
+ async function executeFieldWrite(client, args, paths) {
1011
+ const action = String(args.action);
1012
+ if (action === "save") {
1013
+ validateAggregateSavePayload(args.payload, "ModuleFieldSaveBO", "moduleFieldList");
1014
+ }
1015
+ return executePayloadWrite(client, args, paths);
1016
+ }
1017
+ async function executeRecordWrite(client, args, paths) {
1018
+ const action = String(args.action);
1019
+ if (action === "save") {
1020
+ validateAggregateSavePayload(args.payload, "ModuleFieldDataSaveBO", "fieldDataList");
1021
+ }
1022
+ return executePayloadWrite(client, args, paths);
1023
+ }
957
1024
  async function executeSceneWrite(client, args) {
958
1025
  const action = String(args.action);
959
1026
  const targetPath = action === "delete" || action === "default"
@@ -1026,12 +1093,18 @@ function readonlyResult(targetPath, data, extra = {}) {
1026
1093
  return { readonly: true, targetPath, ...extra, data };
1027
1094
  }
1028
1095
  function requiredId(value, field) {
1096
+ if (typeof value === "number" && !Number.isSafeInteger(value)) {
1097
+ throw new Error(`${field} 超过 JavaScript 安全整数范围,必须以字符串传入。`);
1098
+ }
1029
1099
  const normalized = String(value ?? "").trim();
1030
1100
  if (!/^\d+$/.test(normalized)) {
1031
1101
  throw new Error(`${field} 必须是数字 ID。`);
1032
1102
  }
1033
1103
  return normalized;
1034
1104
  }
1105
+ function isModuleNotFoundError(error) {
1106
+ return error instanceof Error && error.message.includes("模块未找到");
1107
+ }
1035
1108
  function numericValue(value) {
1036
1109
  const numeric = Number(value);
1037
1110
  return Number.isSafeInteger(numeric) ? numeric : value;
@@ -1075,6 +1148,40 @@ function safePayload(value) {
1075
1148
  }
1076
1149
  return value;
1077
1150
  }
1151
+ function applicationPayload(value, action) {
1152
+ const payload = safePayload(value);
1153
+ const name = typeof payload.name === "string" ? payload.name.trim() : "";
1154
+ if (!name) {
1155
+ throw new Error("无代码应用名称不能为空。");
1156
+ }
1157
+ if (Array.from(name).length > 20) {
1158
+ throw new Error("无代码应用名称不能超过 20 个字符。");
1159
+ }
1160
+ const description = payload.description;
1161
+ if (description !== undefined && (typeof description !== "string" || Array.from(description).length > 50)) {
1162
+ throw new Error("无代码应用描述必须是字符串且不能超过 50 个字符。");
1163
+ }
1164
+ const icon = typeof payload.icon === "string" ? payload.icon.trim() : "";
1165
+ if (!icon) {
1166
+ throw new Error("无代码应用图标不能为空。");
1167
+ }
1168
+ // 创建时补齐旧前端提交的非业务默认值;更新仍保留调用方提供的完整应用配置。
1169
+ return action === "create"
1170
+ ? { ...payload, name, icon, description: description ?? "", iconColor: payload.iconColor ?? "#EBECF0", manageUserIds: payload.manageUserIds ?? [] }
1171
+ : { ...payload, name, icon };
1172
+ }
1173
+ function validateAggregateSavePayload(value, boName, listField) {
1174
+ const payload = safePayload(value);
1175
+ requiredId(payload.moduleId, "payload.moduleId");
1176
+ // 新建模块的有效版本通常为 0;不能用 requiredVersion 的默认值掩盖调用方漏传版本。
1177
+ if (payload.version === undefined) {
1178
+ throw new Error(`${boName} 必须包含实际 version;请先读取模块详情。`);
1179
+ }
1180
+ requiredVersion(payload.version);
1181
+ if (!Array.isArray(payload[listField]) || payload[listField].length === 0) {
1182
+ throw new Error(`${boName} 必须包含非空 ${listField}。`);
1183
+ }
1184
+ }
1078
1185
  function cleanObject(input) {
1079
1186
  const output = {};
1080
1187
  for (const [key, value] of Object.entries(input)) {
package/dist/server.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { z } from "zod";
3
+ import { CrmClient } from "./client.js";
3
4
  import { createNocodeToolHandlers, nocodeToolDefinitions } from "./nocode.js";
4
5
  import { createToolHandlers } from "./tools.js";
5
6
  export const CRM_READ_SCOPE = "crm.read";
@@ -181,13 +182,14 @@ export function createWukongMcpServer(config = {}) {
181
182
  const { oauth, ...crmConfig } = config;
182
183
  const server = new McpServer({
183
184
  name: "wukong-mcp",
184
- version: "0.2.0"
185
+ version: "0.2.2"
185
186
  }, {
186
187
  instructions: "先使用只读工具确认模块、记录和字段,再执行写入。所有写入必须显式传 confirm=true;不要尝试任意 URL 请求。"
187
188
  });
189
+ const sharedClient = new CrmClient(crmConfig);
188
190
  const handlers = {
189
- ...createToolHandlers(crmConfig),
190
- ...createNocodeToolHandlers(crmConfig)
191
+ ...createToolHandlers(crmConfig, sharedClient),
192
+ ...createNocodeToolHandlers(crmConfig, sharedClient)
191
193
  };
192
194
  const registeredTools = [
193
195
  ...toolDefinitions.map(([name, description, schema]) => ({
package/dist/tools.d.ts CHANGED
@@ -1,8 +1,8 @@
1
- import { CrmClientConfig } from "./client.js";
1
+ import { CrmClient, CrmClientConfig } from "./client.js";
2
2
  type AnyArgs = Record<string, any>;
3
3
  type Handler = (args: AnyArgs) => Promise<any> | any;
4
4
  export interface ToolHandlers {
5
5
  [name: string]: Handler;
6
6
  }
7
- export declare function createToolHandlers(config?: CrmClientConfig): ToolHandlers;
7
+ export declare function createToolHandlers(config?: CrmClientConfig, sharedClient?: CrmClient): ToolHandlers;
8
8
  export {};
package/dist/tools.js CHANGED
@@ -332,8 +332,8 @@ const WRITE_DOMAINS = {
332
332
  allowedPrefixes: ["/finance"]
333
333
  }
334
334
  };
335
- export function createToolHandlers(config = {}) {
336
- const client = new CrmClient(config);
335
+ export function createToolHandlers(config = {}, sharedClient) {
336
+ const client = sharedClient ?? new CrmClient(config);
337
337
  return {
338
338
  crm_auth_status: async () => client.authStatus(),
339
339
  crm_list_modules: () => ({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wukongcrm/mcp-server",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "Local and remote MCP server for fixed 72CRM API access.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",