@wukongcrm/mcp-server 0.2.1 → 0.2.3

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.3.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.3.tgz
159
159
  ```
160
160
 
161
161
  安装后可以直接用命令启动:
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: "一次获取无代码模块字段、表单布局、列表头、页面布局、选项、公式、标签和序列号规则;写入记录前应先调用,并优先使用 nocode_get_module 返回的实际 version(新建模块通常为 0);只读。",
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
@@ -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) => {
@@ -432,10 +435,22 @@ export function createNocodeToolHandlers(config = {}) {
432
435
  const query = cleanObject({ moduleId: numericValue(moduleId), version, categoryId: args.categoryId });
433
436
  // 旧后端会直接拆箱 filterHidden;显式传 false 与无代码前端的字段配置请求保持一致,避免空值触发 500。
434
437
  const formQuery = { ...query, filterHidden: false };
435
- const [fields, form, listHead, layout, unionModules] = await Promise.all([
438
+ const [fields, form, listHeadOutcome, layout, unionModules] = await Promise.all([
436
439
  client.post("/moduleField/queryList", query),
437
440
  client.post("/moduleField/formList", formQuery),
438
- client.post("/moduleField/queryListHead", query),
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
+ }),
439
454
  client.post(`/moduleM/pageLayout/${moduleId}/${version}`),
440
455
  client.post(`/moduleM/unionModules/${moduleId}/${version}`)
441
456
  ]);
@@ -453,9 +468,10 @@ export function createNocodeToolHandlers(config = {}) {
453
468
  version,
454
469
  fields,
455
470
  form,
456
- listHead,
471
+ listHead: listHeadOutcome.data,
457
472
  layout,
458
473
  unionModules,
474
+ ...(listHeadOutcome.warning ? { warnings: [listHeadOutcome.warning] } : {}),
459
475
  ...extended
460
476
  };
461
477
  },
@@ -513,7 +529,7 @@ export function createNocodeToolHandlers(config = {}) {
513
529
  nocode_query_workflow: async (args) => executeNocodeRead(client, queryWorkflowRoute(args)),
514
530
  nocode_query_followups: async (args) => executeNocodeRead(client, queryFollowUpRoute(args)),
515
531
  nocode_query_configuration: async (args) => executeNocodeRead(client, queryConfigurationRoute(args)),
516
- nocode_query_printing: async (args) => executeNocodeRead(client, queryPrintingRoute(args)),
532
+ nocode_query_printing: async (args) => executePrintingRead(client, args),
517
533
  nocode_query_messages: async (args) => executeNocodeRead(client, queryMessageRoute(args)),
518
534
  nocode_query_integrations: async (args) => executeNocodeRead(client, queryIntegrationRoute(args)),
519
535
  nocode_query_marketplace: async (args) => executeNocodeRead(client, queryMarketplaceRoute(args)),
@@ -566,6 +582,41 @@ export function createNocodeToolHandlers(config = {}) {
566
582
  async function executeNocodeRead(client, route) {
567
583
  return readonlyResult(route.targetPath, await client.post(route.targetPath, route.body, route.query), cleanObject({ operation: route.operation, body: route.body, query: route.query }));
568
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
+ }
569
620
  function executeNocodeWrite(client, args, route) {
570
621
  return confirmedPost(client, args, route.operation, route.targetPath, route.body, route.query);
571
622
  }
@@ -1042,12 +1093,18 @@ function readonlyResult(targetPath, data, extra = {}) {
1042
1093
  return { readonly: true, targetPath, ...extra, data };
1043
1094
  }
1044
1095
  function requiredId(value, field) {
1096
+ if (typeof value === "number" && !Number.isSafeInteger(value)) {
1097
+ throw new Error(`${field} 超过 JavaScript 安全整数范围,必须以字符串传入。`);
1098
+ }
1045
1099
  const normalized = String(value ?? "").trim();
1046
1100
  if (!/^\d+$/.test(normalized)) {
1047
1101
  throw new Error(`${field} 必须是数字 ID。`);
1048
1102
  }
1049
1103
  return normalized;
1050
1104
  }
1105
+ function isModuleNotFoundError(error) {
1106
+ return error instanceof Error && error.message.includes("模块未找到");
1107
+ }
1051
1108
  function numericValue(value) {
1052
1109
  const numeric = Number(value);
1053
1110
  return Number.isSafeInteger(numeric) ? numeric : value;
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";
@@ -20,6 +21,16 @@ const searchFilterSchema = z.object({
20
21
  values: z.array(z.unknown()).optional(),
21
22
  groupId: z.number().optional()
22
23
  }).passthrough();
24
+ const crmCreateRecordSchema = z.object({
25
+ module: moduleSchema,
26
+ entity: z.record(z.unknown()).optional().describe("CRM 实体字段;系统字段放在这里,例如 entity.customerName。"),
27
+ field: z.array(z.record(z.unknown())).optional().describe("CRM 自定义字段列表。"),
28
+ saveType: z.number().optional(),
29
+ fileId: idSchema.optional(),
30
+ aiAnalysis: z.number().optional(),
31
+ member: z.record(z.unknown()).optional(),
32
+ confirm: z.boolean().optional()
33
+ }).strict();
23
34
  const crmUserSearchShape = {
24
35
  ownerUserId: idSchema.optional(),
25
36
  ownerUserIds: z.array(idSchema).optional(),
@@ -137,7 +148,7 @@ const toolDefinitions = [
137
148
  ["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()],
138
149
  ["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()],
139
150
  ["crm_preview_write", "写入前预览和校验,不调用 72CRM 写接口。", passthroughSchema],
140
- ["crm_create_record", "新增线索、客户、联系人、产品、商机;必须 confirm=true 才会写入。", z.object({ module: moduleSchema, confirm: z.boolean().optional() }).passthrough()],
151
+ ["crm_create_record", "新增线索、客户、联系人、产品、商机;系统字段放 entity,自定义字段放 field,不支持把 customerName 放在最外层;必须 confirm=true 才会写入。", crmCreateRecordSchema],
141
152
  ["crm_update_customer", "按客户 ID 或手机号/客户名唯一匹配后更新客户字段;会自动补全字段结构并写后校验,必须 confirm=true 才会写入。", z.object({ confirm: z.boolean().optional() }).passthrough()],
142
153
  ["crm_update_record_fields", "更新线索、客户、联系人、产品、商机基础字段;必须 confirm=true 才会写入。", z.object({ module: moduleSchema, id: z.union([z.string(), z.number()]), confirm: z.boolean().optional() }).passthrough()],
143
154
  ["crm_create_product", "新增 CRM 产品;固定调用 /crmProduct/add,可直接传 72CRM payload 或 entity/field,必须 confirm=true。", z.object({ confirm: z.boolean().optional() }).passthrough()],
@@ -181,13 +192,14 @@ export function createWukongMcpServer(config = {}) {
181
192
  const { oauth, ...crmConfig } = config;
182
193
  const server = new McpServer({
183
194
  name: "wukong-mcp",
184
- version: "0.2.0"
195
+ version: "0.2.3"
185
196
  }, {
186
197
  instructions: "先使用只读工具确认模块、记录和字段,再执行写入。所有写入必须显式传 confirm=true;不要尝试任意 URL 请求。"
187
198
  });
199
+ const sharedClient = new CrmClient(crmConfig);
188
200
  const handlers = {
189
- ...createToolHandlers(crmConfig),
190
- ...createNocodeToolHandlers(crmConfig)
201
+ ...createToolHandlers(crmConfig, sharedClient),
202
+ ...createNocodeToolHandlers(crmConfig, sharedClient)
191
203
  };
192
204
  const registeredTools = [
193
205
  ...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.1",
3
+ "version": "0.2.3",
4
4
  "description": "Local and remote MCP server for fixed 72CRM API access.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",