mes-mcp 0.2.3 → 0.3.0
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/.env.example +5 -0
- package/dist/config.js +10 -0
- package/dist/discovery/action-search.js +0 -0
- package/dist/discovery/action-search.service.js +35 -0
- package/dist/discovery/embedding-provider.js +55 -0
- package/dist/index.js +3 -1
- package/dist/prompts/register-prompts.js +2 -1
- package/dist/resources/register-resources.js +6 -0
- package/dist/tools/register-tools.js +65 -12
- package/package.json +1 -1
package/.env.example
CHANGED
|
@@ -3,3 +3,8 @@ MES_API_PREFIX=/api
|
|
|
3
3
|
MES_AGENT_TOKEN=
|
|
4
4
|
MES_TIMEOUT_MS=30000
|
|
5
5
|
MES_REGISTER_DYNAMIC_TOOLS=false
|
|
6
|
+
# mes_action.list 动作检索模式:
|
|
7
|
+
# lexical(默认,推荐)= CJK 词元 BM25 + 双语领域词扩展,离线、零额外依赖、确定性;
|
|
8
|
+
# hybrid / semantic = 额外融合本地语义向量,需手动安装可选依赖 @huggingface/transformers
|
|
9
|
+
# (含原生 onnxruntime,部分受限环境装不上,届时自动退化为 lexical)。
|
|
10
|
+
MES_ACTION_SEARCH=lexical
|
package/dist/config.js
CHANGED
|
@@ -24,6 +24,15 @@ function optionalBooleanEnv(name, fallback) {
|
|
|
24
24
|
return false;
|
|
25
25
|
throw new Error(`${name} must be a boolean`);
|
|
26
26
|
}
|
|
27
|
+
function actionSearchModeEnv(name, fallback) {
|
|
28
|
+
const value = process.env[name]?.trim().toLowerCase();
|
|
29
|
+
if (!value)
|
|
30
|
+
return fallback;
|
|
31
|
+
if (value === "lexical" || value === "hybrid" || value === "semantic") {
|
|
32
|
+
return value;
|
|
33
|
+
}
|
|
34
|
+
throw new Error(`${name} must be one of lexical | hybrid | semantic`);
|
|
35
|
+
}
|
|
27
36
|
function decodeBase64UrlJson(segment) {
|
|
28
37
|
try {
|
|
29
38
|
const normalized = segment.replace(/-/g, "+").replace(/_/g, "/");
|
|
@@ -86,6 +95,7 @@ export function loadConfig() {
|
|
|
86
95
|
mesAgentToken,
|
|
87
96
|
timeoutMs,
|
|
88
97
|
registerDynamicTools: optionalBooleanEnv("MES_REGISTER_DYNAMIC_TOOLS", false),
|
|
98
|
+
actionSearchMode: actionSearchModeEnv("MES_ACTION_SEARCH", "lexical"),
|
|
89
99
|
tokenInfo,
|
|
90
100
|
expectedUsername,
|
|
91
101
|
expectedAgentClientId,
|
|
Binary file
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { ActionSearchIndex, } from "./action-search.js";
|
|
2
|
+
import { createEmbeddingProvider } from "./embedding-provider.js";
|
|
3
|
+
/**
|
|
4
|
+
* 包装「全量动作缓存 + 相关性索引」。索引在首次检索时懒构建并缓存。
|
|
5
|
+
*
|
|
6
|
+
* `client.listBusinessActions()` 已按当前 AI 凭证 + 账号 RBAC 过滤过(后端动作目录过滤逻辑),
|
|
7
|
+
* 所以在这份缓存上检索天然遵守权限,不会暴露当前账号无权的动作。
|
|
8
|
+
*/
|
|
9
|
+
export class ActionSearchService {
|
|
10
|
+
client;
|
|
11
|
+
mode;
|
|
12
|
+
indexPromise;
|
|
13
|
+
constructor(client, mode) {
|
|
14
|
+
this.client = client;
|
|
15
|
+
this.mode = mode;
|
|
16
|
+
}
|
|
17
|
+
async buildIndex() {
|
|
18
|
+
const [actions, provider] = await Promise.all([
|
|
19
|
+
this.client.listBusinessActions(),
|
|
20
|
+
createEmbeddingProvider(this.mode),
|
|
21
|
+
]);
|
|
22
|
+
return new ActionSearchIndex(actions, provider);
|
|
23
|
+
}
|
|
24
|
+
async search(query, options) {
|
|
25
|
+
if (!this.indexPromise) {
|
|
26
|
+
this.indexPromise = this.buildIndex().catch((error) => {
|
|
27
|
+
this.indexPromise = undefined;
|
|
28
|
+
throw error;
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
const index = await this.indexPromise;
|
|
32
|
+
const results = await index.search(query, options);
|
|
33
|
+
return { total: index.size, results };
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 本地语义向量 provider。
|
|
3
|
+
*
|
|
4
|
+
* 使用 `@huggingface/transformers` 的 all-MiniLM-L6-v2(384 维),完全本地运行、离线、
|
|
5
|
+
* 业务数据不出机器。该依赖在 package.json 中声明为 optionalDependencies:
|
|
6
|
+
* - 装上了 + 模式为 hybrid/semantic → 启用语义融合;
|
|
7
|
+
* - 没装(受限运行时)或模式为 lexical → 返回 null,检索退化为纯 CJK 词法 BM25(仍然可用)。
|
|
8
|
+
*
|
|
9
|
+
* 这里的「装不上就退化为词法」是检索排序能力的降级(并在 stderr 明示),
|
|
10
|
+
* 不是用兜底数据掩盖业务错误——词法检索本身是完整可用的检索实现。
|
|
11
|
+
*/
|
|
12
|
+
// 多语言模型,覆盖中文业务意图(MES 动作标题/描述以中文为主)。
|
|
13
|
+
const MODEL_ID = "Xenova/paraphrase-multilingual-MiniLM-L12-v2";
|
|
14
|
+
// 用变量 specifier 引入可选依赖,避免 tsc 在未安装时报「找不到模块」。
|
|
15
|
+
const TRANSFORMERS_MODULE = "@huggingface/transformers";
|
|
16
|
+
async function importTransformers() {
|
|
17
|
+
return (await import(TRANSFORMERS_MODULE));
|
|
18
|
+
}
|
|
19
|
+
class LocalEmbeddingProvider {
|
|
20
|
+
pipelinePromise = null;
|
|
21
|
+
async getPipeline() {
|
|
22
|
+
if (!this.pipelinePromise) {
|
|
23
|
+
this.pipelinePromise = (async () => {
|
|
24
|
+
const mod = await importTransformers();
|
|
25
|
+
return mod.pipeline("feature-extraction", MODEL_ID);
|
|
26
|
+
})();
|
|
27
|
+
}
|
|
28
|
+
return this.pipelinePromise;
|
|
29
|
+
}
|
|
30
|
+
async embed(texts) {
|
|
31
|
+
if (texts.length === 0)
|
|
32
|
+
return [];
|
|
33
|
+
const extractor = await this.getPipeline();
|
|
34
|
+
const output = await extractor(texts, { pooling: "mean", normalize: true });
|
|
35
|
+
return output.tolist();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* 按检索模式创建 provider:
|
|
40
|
+
* - lexical → 始终返回 null(纯词法)。
|
|
41
|
+
* - hybrid/semantic → 探测可选向量库是否可用;可用则返回本地 provider,否则 null + 警告。
|
|
42
|
+
*/
|
|
43
|
+
export async function createEmbeddingProvider(mode) {
|
|
44
|
+
if (mode === "lexical")
|
|
45
|
+
return null;
|
|
46
|
+
try {
|
|
47
|
+
// 探测可选依赖是否安装;未安装时 import 抛错,退化为词法。
|
|
48
|
+
await importTransformers();
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
console.error(`[mes-mcp] MES_ACTION_SEARCH=${mode} 需要可选依赖 @huggingface/transformers,未检测到,已退化为词法检索(lexical)。如需语义检索,请在 mes-mcp 安装该依赖后重启。`);
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
return new LocalEmbeddingProvider();
|
|
55
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -27,6 +27,7 @@ function logStartupDiagnostics(config) {
|
|
|
27
27
|
`[mes-mcp] endpoint=${config.mesBaseUrl}${config.mesApiPrefix}`,
|
|
28
28
|
`[mes-mcp] token username=${config.tokenInfo.username ?? "<unknown>"} apiKeyId=${maskId(config.tokenInfo.apiKeyId)} agentClientId=${config.tokenInfo.agentClientId ?? "<unknown>"} expiresAt=${formatUnixSeconds(config.tokenInfo.expiresAt)}`,
|
|
29
29
|
`[mes-mcp] dynamicTools=${config.registerDynamicTools ? "enabled" : "disabled"} (${config.registerDynamicTools ? "mes.<actionCode> tools will be registered" : "use mes_action.list/detail/execute; set MES_REGISTER_DYNAMIC_TOOLS=true only when the client needs expanded tools"})`,
|
|
30
|
+
`[mes-mcp] actionSearch=${config.actionSearchMode} (mes_action.list 按相关性检索动作目录;hybrid/semantic 需安装可选依赖 @huggingface/transformers,否则自动退化为 lexical)`,
|
|
30
31
|
];
|
|
31
32
|
if (config.expectedUsername) {
|
|
32
33
|
lines.push(`[mes-mcp] expected username=${config.expectedUsername}`);
|
|
@@ -45,10 +46,11 @@ async function main() {
|
|
|
45
46
|
const client = new MesAgentApiClient(config);
|
|
46
47
|
const server = new McpServer({
|
|
47
48
|
name: "mes-mcp",
|
|
48
|
-
version: "0.
|
|
49
|
+
version: "0.3.0",
|
|
49
50
|
});
|
|
50
51
|
await registerAllAgentTools(server, client, {
|
|
51
52
|
registerDynamicTools: config.registerDynamicTools,
|
|
53
|
+
actionSearchMode: config.actionSearchMode,
|
|
52
54
|
});
|
|
53
55
|
registerMesResources(server, client);
|
|
54
56
|
registerMesPrompts(server);
|
|
@@ -15,10 +15,11 @@ export function registerMesPrompts(server) {
|
|
|
15
15
|
text: [
|
|
16
16
|
"你是 MES 业务助手,必须按当前账号权限和工具返回结果执行。",
|
|
17
17
|
"不要猜测客户、物料、BOM、工艺路线、仓库等关键主数据;缺失时先查询或要求用户确认。",
|
|
18
|
-
"
|
|
18
|
+
"动作目录有数百个动作。发现动作时用 mes_action.list,把你当前要做的事用一句自然语言意图传给 keyword(例如「销售单转生产计划」「采购入库」「报工」),它返回按相关性排序的 top-K 动作;再用 mes_action.detail 查看字段,最后用 mes_action.execute 执行。不要不带意图地翻页或猜路径,也不要依赖工具列表里必须存在 mes.<动作编码>。",
|
|
19
19
|
"mes_action.execute 返回的是执行信封,真实业务返回在 data 字段;confirm_required 产生的 invocationId 必须由同一公司/租户下有权限的账号审批。",
|
|
20
20
|
"mes_api.read 只用于普通业务只读接口,pageSize 最大 100;如果返回 _mcpWarnings 或提示需要动态动作,请按提示调整。",
|
|
21
21
|
"销售到出库的流程也走动态动作:先查客户和物料,再创建销售单、处理审批、确认销售单、转生产计划、排产或生成工单、报工、质检、发货出库。",
|
|
22
|
+
"库存盘点差异优先使用 stock.stock_count.create/update/start/submit/approve 盘点单流程;stock.batch_adjust 只适合非批次物料的简化批量差异调整,批次追踪物料请使用 stock.adjust 并指定 batchNo。",
|
|
22
23
|
"写入类动作默认先使用 confirm_required 或 preview;preview 不写入,confirm_required 生成待确认记录,commit 只有凭证允许时才真实提交。",
|
|
23
24
|
context ? `当前上下文:${context}` : "",
|
|
24
25
|
]
|
|
@@ -57,6 +57,12 @@ export function registerMesResources(server, client) {
|
|
|
57
57
|
"- MES 分页 `pageSize` 最大为 100;MCP 会裁剪超限值,并通过 `_mcpWarnings` 返回提示。",
|
|
58
58
|
"- 如果只读路径被提示需要动态业务动作,请改用 `mes_action.list/detail/execute` 查找并调用对应 actionCode。",
|
|
59
59
|
"",
|
|
60
|
+
"## 库存盘点和调整",
|
|
61
|
+
"",
|
|
62
|
+
"- 正式库存盘点差异优先走 `stock.stock_count.create` -> `stock.stock_count.update` -> `stock.stock_count.start` -> `stock.stock_count.submit` -> `stock.stock_count.approve`。",
|
|
63
|
+
"- `stock.out` 只用于无来源直接出库,类型限定为 `out_scrap` 或 `out_other`;销售发货不要绕过销售出库单。",
|
|
64
|
+
"- `stock.batch_adjust` 只适合非批次物料的简化批量差异调整;批次追踪物料请使用 `stock.adjust` 并传 `batchNo`。",
|
|
65
|
+
"",
|
|
60
66
|
"## 防误连",
|
|
61
67
|
"",
|
|
62
68
|
"- MCP 启动时会在 stderr 打印 MES 地址、Token 用户名、apiKeyId、agentClientId 和动态工具状态。",
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
+
import { ActionSearchService } from "../discovery/action-search.service.js";
|
|
3
4
|
const MAX_MES_PAGE_SIZE = 100;
|
|
5
|
+
const DEFAULT_ACTION_LIST_TOPK = 20;
|
|
6
|
+
const MAX_ACTION_LIST_TOPK = 50;
|
|
4
7
|
const ExecutionModeSchema = z
|
|
5
8
|
.enum(["preview", "confirm_required", "commit"])
|
|
6
9
|
.optional();
|
|
@@ -174,15 +177,39 @@ function actionDescription(action) {
|
|
|
174
177
|
.filter(Boolean)
|
|
175
178
|
.join("\n");
|
|
176
179
|
}
|
|
177
|
-
function
|
|
180
|
+
function listActionView(action, relevance) {
|
|
181
|
+
return {
|
|
182
|
+
actionCode: action.actionCode,
|
|
183
|
+
title: action.title,
|
|
184
|
+
description: action.description,
|
|
185
|
+
module: action.module,
|
|
186
|
+
resource: action.resource,
|
|
187
|
+
operation: action.operation,
|
|
188
|
+
path: action.path,
|
|
189
|
+
method: action.method,
|
|
190
|
+
isWrite: action.isWrite,
|
|
191
|
+
permissions: action.permissions,
|
|
192
|
+
riskLevel: action.riskLevel,
|
|
193
|
+
relevance: Number(relevance.toFixed(4)),
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
function registerGenericActionTools(server, client, searchService) {
|
|
178
197
|
server.registerTool("mes_action.list", {
|
|
179
|
-
title: "
|
|
180
|
-
description: "
|
|
198
|
+
title: "Search MES business actions",
|
|
199
|
+
description: "按相关性检索当前 AI 凭证和账号权限允许调用的 MES 页面业务动作。动作目录有数百个,请把要做的事用一句自然语言意图放进 keyword(例如「销售单转生产计划」「采购入库」「报工」),返回的是按相关性排序的 top-K 动作(不是分页字母序)。可选 module/resource/operation/write 做精确过滤。拿到 actionCode 后用 mes_action.detail 看字段、mes_action.execute 执行。",
|
|
181
200
|
inputSchema: z
|
|
182
201
|
.object({
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
202
|
+
keyword: z
|
|
203
|
+
.string()
|
|
204
|
+
.optional()
|
|
205
|
+
.describe("要做的事的自然语言意图,用于相关性检索"),
|
|
206
|
+
topK: z
|
|
207
|
+
.number()
|
|
208
|
+
.int()
|
|
209
|
+
.positive()
|
|
210
|
+
.max(MAX_ACTION_LIST_TOPK)
|
|
211
|
+
.optional()
|
|
212
|
+
.describe("返回的最相关动作数量,默认 20,最大 50"),
|
|
186
213
|
module: z.string().optional(),
|
|
187
214
|
resource: z.string().optional(),
|
|
188
215
|
operation: z.string().optional(),
|
|
@@ -198,9 +225,8 @@ function registerGenericActionTools(server, client) {
|
|
|
198
225
|
return runTool({ toolName: "mes_action.list" }, async () => {
|
|
199
226
|
const parsed = z
|
|
200
227
|
.object({
|
|
201
|
-
page: z.number().int().positive().optional(),
|
|
202
|
-
pageSize: z.number().int().positive().optional(),
|
|
203
228
|
keyword: z.string().optional(),
|
|
229
|
+
topK: z.number().int().positive().optional(),
|
|
204
230
|
module: z.string().optional(),
|
|
205
231
|
resource: z.string().optional(),
|
|
206
232
|
operation: z.string().optional(),
|
|
@@ -209,9 +235,35 @@ function registerGenericActionTools(server, client) {
|
|
|
209
235
|
.passthrough()
|
|
210
236
|
.parse(args ?? {});
|
|
211
237
|
const warnings = [];
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
238
|
+
let topK = parsed.topK ?? DEFAULT_ACTION_LIST_TOPK;
|
|
239
|
+
if (topK > MAX_ACTION_LIST_TOPK) {
|
|
240
|
+
warnings.push({
|
|
241
|
+
code: "topK_clamped",
|
|
242
|
+
message: `topK 最大为 ${MAX_ACTION_LIST_TOPK},已从 ${topK} 调整为 ${MAX_ACTION_LIST_TOPK}。`,
|
|
243
|
+
});
|
|
244
|
+
topK = MAX_ACTION_LIST_TOPK;
|
|
245
|
+
}
|
|
246
|
+
const keyword = parsed.keyword?.trim() ?? "";
|
|
247
|
+
const { total, results } = await searchService.search(keyword, {
|
|
248
|
+
module: parsed.module,
|
|
249
|
+
resource: parsed.resource,
|
|
250
|
+
operation: parsed.operation,
|
|
251
|
+
write: parsed.write,
|
|
252
|
+
topK,
|
|
253
|
+
});
|
|
254
|
+
if (keyword && results.length === 0) {
|
|
255
|
+
warnings.push({
|
|
256
|
+
code: "no_match",
|
|
257
|
+
message: "没有命中相关动作。换一个更贴近业务的意图词(如把「转计划」换成「销售单转生产计划」),或用 module/operation 过滤。",
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
return formatToolResult({
|
|
261
|
+
query: keyword || null,
|
|
262
|
+
ranked: Boolean(keyword),
|
|
263
|
+
total,
|
|
264
|
+
returned: results.length,
|
|
265
|
+
list: results.map((r) => listActionView(r.action, r.score)),
|
|
266
|
+
}, warnings);
|
|
215
267
|
});
|
|
216
268
|
});
|
|
217
269
|
server.registerTool("mes_action.detail", {
|
|
@@ -312,8 +364,9 @@ async function registerDynamicBusinessActionTools(server, client) {
|
|
|
312
364
|
}
|
|
313
365
|
}
|
|
314
366
|
export async function registerAllAgentTools(server, client, options = {}) {
|
|
367
|
+
const searchService = new ActionSearchService(client, options.actionSearchMode ?? "lexical");
|
|
315
368
|
registerAgentTools(server, client);
|
|
316
|
-
registerGenericActionTools(server, client);
|
|
369
|
+
registerGenericActionTools(server, client, searchService);
|
|
317
370
|
if (!options.registerDynamicTools)
|
|
318
371
|
return;
|
|
319
372
|
try {
|