@wukongcrm/mcp-server 0.2.1 → 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 +2 -2
- package/dist/client.d.ts +1 -0
- package/dist/client.js +9 -1
- package/dist/nocode.d.ts +2 -2
- package/dist/nocode.js +66 -9
- package/dist/server.js +5 -3
- package/dist/tools.d.ts +2 -2
- package/dist/tools.js +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -147,7 +147,7 @@ npm pack
|
|
|
147
147
|
生成文件示例:
|
|
148
148
|
|
|
149
149
|
```text
|
|
150
|
-
wukongcrm-mcp-server-0.2.
|
|
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.
|
|
158
|
+
npm install -g .\wukongcrm-mcp-server-0.2.2.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
|
-
|
|
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([
|
|
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: "
|
|
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,
|
|
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) =>
|
|
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";
|
|
@@ -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.
|
|
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: () => ({
|