mes-mcp 0.3.1 → 0.3.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/.env.example
CHANGED
|
@@ -2,6 +2,9 @@ MES_BASE_URL=http://127.0.0.1:6033
|
|
|
2
2
|
MES_API_PREFIX=/api
|
|
3
3
|
MES_AGENT_TOKEN=
|
|
4
4
|
MES_TIMEOUT_MS=30000
|
|
5
|
+
# 动作目录缓存 TTL(毫秒,兜底),默认 12h。另外跨自然日必刷新:
|
|
6
|
+
# 每天第一次调用 mes_action.* 时自动拉最新动作目录;用户也可用 mes_action.catalog 手动强刷。
|
|
7
|
+
MES_ACTION_CACHE_TTL_MS=43200000
|
|
5
8
|
MES_REGISTER_DYNAMIC_TOOLS=false
|
|
6
9
|
# mes_action.list 动作检索模式:
|
|
7
10
|
# lexical(默认,推荐)= CJK 词元 BM25 + 双语领域词扩展,离线、零额外依赖、确定性;
|
|
@@ -26,6 +26,9 @@ function buildMesErrorMessage(path, status, message) {
|
|
|
26
26
|
export class MesAgentApiClient {
|
|
27
27
|
config;
|
|
28
28
|
businessActionsPromise;
|
|
29
|
+
// 动作目录缓存最近成功刷新时间;驱动每日首用/TTL 自动刷新,并对外报告。
|
|
30
|
+
// undefined 且 promise 存在 = 拉取在途(并发复用同一 promise,避免重复请求)。
|
|
31
|
+
businessActionsFetchedAt;
|
|
29
32
|
constructor(config) {
|
|
30
33
|
this.config = config;
|
|
31
34
|
}
|
|
@@ -68,15 +71,46 @@ export class MesAgentApiClient {
|
|
|
68
71
|
return this.post(normalized, payload);
|
|
69
72
|
}
|
|
70
73
|
async listBusinessActions(options = {}) {
|
|
71
|
-
|
|
74
|
+
// 复用:非强制刷新 + 有缓存/在途 + (在途未定时间 或 仍新鲜)
|
|
75
|
+
if (!options.refresh &&
|
|
76
|
+
this.businessActionsPromise &&
|
|
77
|
+
(this.businessActionsFetchedAt === undefined || this.isCatalogFresh())) {
|
|
72
78
|
return this.businessActionsPromise;
|
|
73
79
|
}
|
|
74
|
-
|
|
80
|
+
// 开新拉取:先清 fetchedAt 标记「在途」,并发调用复用同一 promise 不重复请求
|
|
81
|
+
this.businessActionsFetchedAt = undefined;
|
|
82
|
+
this.businessActionsPromise = this.fetchBusinessActions()
|
|
83
|
+
.then((actions) => {
|
|
84
|
+
this.businessActionsFetchedAt = new Date();
|
|
85
|
+
return actions;
|
|
86
|
+
})
|
|
87
|
+
.catch((error) => {
|
|
75
88
|
this.businessActionsPromise = undefined;
|
|
89
|
+
this.businessActionsFetchedAt = undefined;
|
|
76
90
|
throw error;
|
|
77
91
|
});
|
|
78
92
|
return this.businessActionsPromise;
|
|
79
93
|
}
|
|
94
|
+
/**
|
|
95
|
+
* 动作目录缓存是否仍新鲜:跨自然日或超过 TTL 视为过期。
|
|
96
|
+
* 跨自然日过期即「每天第一次使用自动拉最新目录」。
|
|
97
|
+
*/
|
|
98
|
+
isCatalogFresh() {
|
|
99
|
+
if (!this.businessActionsFetchedAt)
|
|
100
|
+
return false;
|
|
101
|
+
const now = new Date();
|
|
102
|
+
if (now.toDateString() !== this.businessActionsFetchedAt.toDateString()) {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
return (now.getTime() - this.businessActionsFetchedAt.getTime() <
|
|
106
|
+
this.config.actionCacheTtlMs);
|
|
107
|
+
}
|
|
108
|
+
/** 动作目录最近一次成功刷新时间(ISO);未拉取过返回 null。 */
|
|
109
|
+
businessActionsRefreshedAt() {
|
|
110
|
+
return this.businessActionsFetchedAt
|
|
111
|
+
? this.businessActionsFetchedAt.toISOString()
|
|
112
|
+
: null;
|
|
113
|
+
}
|
|
80
114
|
async getBusinessAction(actionCode) {
|
|
81
115
|
const actions = await this.listBusinessActions();
|
|
82
116
|
const cached = actions.find((action) => action.actionCode === actionCode);
|
package/dist/config.js
CHANGED
|
@@ -81,6 +81,10 @@ export function loadConfig() {
|
|
|
81
81
|
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
|
|
82
82
|
throw new Error("MES_TIMEOUT_MS must be a positive number");
|
|
83
83
|
}
|
|
84
|
+
const actionCacheTtlMs = Number(optionalEnv("MES_ACTION_CACHE_TTL_MS", "43200000"));
|
|
85
|
+
if (!Number.isFinite(actionCacheTtlMs) || actionCacheTtlMs <= 0) {
|
|
86
|
+
throw new Error("MES_ACTION_CACHE_TTL_MS must be a positive number");
|
|
87
|
+
}
|
|
84
88
|
const mesAgentToken = requiredEnv("MES_AGENT_TOKEN");
|
|
85
89
|
const tokenInfo = decodeAgentToken(mesAgentToken);
|
|
86
90
|
const expectedUsername = optionalRawEnv("MES_EXPECTED_USERNAME");
|
|
@@ -96,6 +100,7 @@ export function loadConfig() {
|
|
|
96
100
|
timeoutMs,
|
|
97
101
|
registerDynamicTools: optionalBooleanEnv("MES_REGISTER_DYNAMIC_TOOLS", false),
|
|
98
102
|
actionSearchMode: actionSearchModeEnv("MES_ACTION_SEARCH", "lexical"),
|
|
103
|
+
actionCacheTtlMs,
|
|
99
104
|
tokenInfo,
|
|
100
105
|
expectedUsername,
|
|
101
106
|
expectedAgentClientId,
|
|
@@ -15,7 +15,8 @@ export function registerMesPrompts(server) {
|
|
|
15
15
|
text: [
|
|
16
16
|
"你是 MES 业务助手,必须按当前账号权限和工具返回结果执行。",
|
|
17
17
|
"不要猜测客户、物料、BOM、工艺路线、仓库等关键主数据;缺失时先查询或要求用户确认。",
|
|
18
|
-
"
|
|
18
|
+
"处理 MES 任务前、或用户说「更新一下 mes / 看看有哪些动作」时,先调用 mes_action.catalog 加载当前账号权限内的全部动作紧凑目录(它会强制刷新;mcp 每天第一次使用也会自动拉最新目录),把可用动作装进上下文。",
|
|
19
|
+
"动作目录有数百个动作。已加载目录后要执行具体某件事,用 mes_action.list 把意图用一句自然语言传给 keyword(例如「销售单转生产计划」「采购入库」「报工」)拿到相关性 top-K,再用 mes_action.detail 查看字段。遇到前置步骤、字段归属或业务口径不清楚时,先用 mes_manual.search 或 mes_workflow.detail 查后端手册索引,最后用 mes_action.execute 执行。不要不带意图地翻页或猜路径,也不要依赖工具列表里必须存在 mes.<动作编码>。",
|
|
19
20
|
"mes_action.execute 返回的是执行信封,真实业务返回在 data 字段;confirm_required 产生的 invocationId 必须由同一公司/租户下有权限的账号审批。",
|
|
20
21
|
"mes_api.read 只用于普通业务只读接口,pageSize 最大 100;如果返回 _mcpWarnings 或提示需要动态动作,请按提示调整。",
|
|
21
22
|
"销售到出库的流程也走动态动作:先查客户和物料,再创建销售单、处理审批、确认销售单、转生产计划、排产或生成工单、报工、质检、生产入库/库存确认、发货出库。",
|
|
@@ -37,10 +37,11 @@ export function registerMesResources(server, client) {
|
|
|
37
37
|
"## 工具使用顺序",
|
|
38
38
|
"",
|
|
39
39
|
"1. 使用 `mes_action.list` 或 `mes://agent/actions` 发现当前凭证和账号权限允许的动作。",
|
|
40
|
-
"2. 使用 `mes_action.detail` 查看目标动作的路径、权限、DTO
|
|
41
|
-
"3.
|
|
42
|
-
"4.
|
|
43
|
-
"5.
|
|
40
|
+
"2. 使用 `mes_action.detail` 查看目标动作的路径、权限、DTO 名称、输入 schema、字段归属和推荐工作流提示。",
|
|
41
|
+
"3. 如果业务链路或字段归属不清楚,使用 `mes_manual.search` / `mes_manual.get` / `mes_workflow.detail` 查询后端提供的 AI 操作手册索引。",
|
|
42
|
+
"4. 只读查询使用 `mes_api.read` 调用普通业务只读 POST 接口。",
|
|
43
|
+
"5. 写入、确认、下达、报工、质检、出入库等动作使用 `mes_action.execute` 或可选的 `mes.<动作编码>`。",
|
|
44
|
+
"6. `MES_REGISTER_DYNAMIC_TOOLS=false` 是默认推荐配置;此时工具列表包含 `mes_api.read`、`mes_action.list/detail/execute`、`mes_manual.search/get`、`mes_workflow.detail` 等基础工具。只有客户端必须直观看到 `mes.<动作编码>` 时才开启动态工具展开。",
|
|
44
45
|
"",
|
|
45
46
|
"## 写入规则",
|
|
46
47
|
"",
|
|
@@ -193,6 +193,21 @@ function listActionView(action, relevance) {
|
|
|
193
193
|
relevance: Number(relevance.toFixed(4)),
|
|
194
194
|
};
|
|
195
195
|
}
|
|
196
|
+
// 全量目录用的紧凑视图:只保留识别动作所需字段,不含 path/method/权限/schema,控上下文预算。
|
|
197
|
+
function catalogActionView(action) {
|
|
198
|
+
const brief = action.description?.trim();
|
|
199
|
+
return {
|
|
200
|
+
actionCode: action.actionCode,
|
|
201
|
+
title: action.title,
|
|
202
|
+
operation: action.operation,
|
|
203
|
+
resource: action.resource,
|
|
204
|
+
isWrite: action.isWrite,
|
|
205
|
+
riskLevel: action.riskLevel,
|
|
206
|
+
...(brief
|
|
207
|
+
? { brief: brief.length > 80 ? `${brief.slice(0, 80)}…` : brief }
|
|
208
|
+
: {}),
|
|
209
|
+
};
|
|
210
|
+
}
|
|
196
211
|
function registerGenericActionTools(server, client, searchService) {
|
|
197
212
|
server.registerTool("mes_action.list", {
|
|
198
213
|
title: "Search MES business actions",
|
|
@@ -266,9 +281,70 @@ function registerGenericActionTools(server, client, searchService) {
|
|
|
266
281
|
}, warnings);
|
|
267
282
|
});
|
|
268
283
|
});
|
|
284
|
+
server.registerTool("mes_action.catalog", {
|
|
285
|
+
title: "Load full MES action catalog",
|
|
286
|
+
description: "拉取并刷新当前 AI 凭证和账号权限内【全部】可执行的 MES 页面业务动作紧凑目录(按模块分组),用于把可用动作一次性加载进上下文。当用户说「更新一下 mes / 看看有哪些动作」、你不确定有哪些动作、或距上次加载较久怀疑目录已变时调用。只返回动作码/名称/模块/操作/读写/一句话用途;执行某动作前用 mes_action.detail 看字段,按意图精准找某个动作用 mes_action.list。",
|
|
287
|
+
inputSchema: z
|
|
288
|
+
.object({
|
|
289
|
+
module: z
|
|
290
|
+
.string()
|
|
291
|
+
.optional()
|
|
292
|
+
.describe("只加载某模块的动作(如 sales/purchase/finance),控上下文;不传=全部模块"),
|
|
293
|
+
})
|
|
294
|
+
.passthrough(),
|
|
295
|
+
annotations: {
|
|
296
|
+
readOnlyHint: true,
|
|
297
|
+
destructiveHint: false,
|
|
298
|
+
idempotentHint: true,
|
|
299
|
+
},
|
|
300
|
+
}, async (args) => {
|
|
301
|
+
return runTool({ toolName: "mes_action.catalog" }, async () => {
|
|
302
|
+
const parsed = z
|
|
303
|
+
.object({ module: z.string().optional() })
|
|
304
|
+
.passthrough()
|
|
305
|
+
.parse(args ?? {});
|
|
306
|
+
const moduleFilter = parsed.module?.trim() || undefined;
|
|
307
|
+
// 强制刷新,保证「更新一下 mes」拿到最新目录
|
|
308
|
+
const all = await client.listBusinessActions({ refresh: true });
|
|
309
|
+
const filtered = moduleFilter
|
|
310
|
+
? all.filter((a) => a.module === moduleFilter)
|
|
311
|
+
: all;
|
|
312
|
+
const warnings = [];
|
|
313
|
+
if (moduleFilter && filtered.length === 0) {
|
|
314
|
+
warnings.push({
|
|
315
|
+
code: "module_no_match",
|
|
316
|
+
message: `模块「${moduleFilter}」下没有可用动作;不传 module 可加载全部模块,或用 mes_action.list 按意图检索。`,
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
const byModule = new Map();
|
|
320
|
+
for (const action of filtered) {
|
|
321
|
+
const list = byModule.get(action.module) ?? [];
|
|
322
|
+
list.push(action);
|
|
323
|
+
byModule.set(action.module, list);
|
|
324
|
+
}
|
|
325
|
+
const modules = [...byModule.entries()]
|
|
326
|
+
.sort((a, b) => a[0].localeCompare(b[0]))
|
|
327
|
+
.map(([module, actions]) => ({
|
|
328
|
+
module,
|
|
329
|
+
count: actions.length,
|
|
330
|
+
actions: actions
|
|
331
|
+
.slice()
|
|
332
|
+
.sort((a, b) => a.actionCode.localeCompare(b.actionCode))
|
|
333
|
+
.map(catalogActionView),
|
|
334
|
+
}));
|
|
335
|
+
return formatToolResult({
|
|
336
|
+
refreshedAt: client.businessActionsRefreshedAt(),
|
|
337
|
+
total: filtered.length,
|
|
338
|
+
moduleCount: modules.length,
|
|
339
|
+
filteredModule: moduleFilter ?? null,
|
|
340
|
+
modules,
|
|
341
|
+
note: "以上为当前凭证权限内的全部可用 MES 动作紧凑目录。执行前用 mes_action.detail 看字段/schema;按意图精准找用 mes_action.list。",
|
|
342
|
+
}, warnings);
|
|
343
|
+
});
|
|
344
|
+
});
|
|
269
345
|
server.registerTool("mes_action.detail", {
|
|
270
346
|
title: "Get MES business action detail",
|
|
271
|
-
description: "查询单个 MES 页面业务动作的路径、权限、DTO
|
|
347
|
+
description: "查询单个 MES 页面业务动作的路径、权限、DTO 名称、输入 schema,以及后端提供的手册引用、字段归属和推荐工作流提示。",
|
|
272
348
|
inputSchema: z.object({
|
|
273
349
|
actionCode: z.string().min(1),
|
|
274
350
|
}),
|
|
@@ -288,6 +364,100 @@ function registerGenericActionTools(server, client, searchService) {
|
|
|
288
364
|
return formatToolResult(result);
|
|
289
365
|
});
|
|
290
366
|
});
|
|
367
|
+
server.registerTool("mes_manual.search", {
|
|
368
|
+
title: "Search MES operation manual",
|
|
369
|
+
description: "检索后端提供的 MES AI 操作手册索引,用于补充字段归属、前置动作、推荐流程和常见误用。适合在只看 action schema 不确定业务链路时调用。",
|
|
370
|
+
inputSchema: z
|
|
371
|
+
.object({
|
|
372
|
+
query: z.string().optional().describe("自然语言查询词"),
|
|
373
|
+
keyword: z.string().optional().describe("兼容字段:自然语言查询词"),
|
|
374
|
+
module: z.string().optional().describe("业务模块,如 schedule"),
|
|
375
|
+
actionCode: z
|
|
376
|
+
.string()
|
|
377
|
+
.optional()
|
|
378
|
+
.describe("相关动作编码,如 schedule.plan.auto"),
|
|
379
|
+
field: z
|
|
380
|
+
.string()
|
|
381
|
+
.optional()
|
|
382
|
+
.describe("字段名或字段归属,如 planStartDate"),
|
|
383
|
+
page: z.number().int().positive().optional(),
|
|
384
|
+
pageSize: z
|
|
385
|
+
.number()
|
|
386
|
+
.int()
|
|
387
|
+
.positive()
|
|
388
|
+
.max(MAX_MES_PAGE_SIZE)
|
|
389
|
+
.optional(),
|
|
390
|
+
})
|
|
391
|
+
.passthrough(),
|
|
392
|
+
annotations: {
|
|
393
|
+
readOnlyHint: true,
|
|
394
|
+
destructiveHint: false,
|
|
395
|
+
idempotentHint: true,
|
|
396
|
+
},
|
|
397
|
+
}, async (args) => {
|
|
398
|
+
return runTool({ toolName: "mes_manual.search" }, async () => {
|
|
399
|
+
const parsed = z
|
|
400
|
+
.object({
|
|
401
|
+
query: z.string().optional(),
|
|
402
|
+
keyword: z.string().optional(),
|
|
403
|
+
module: z.string().optional(),
|
|
404
|
+
actionCode: z.string().optional(),
|
|
405
|
+
field: z.string().optional(),
|
|
406
|
+
page: z.number().int().positive().optional(),
|
|
407
|
+
pageSize: z.number().int().positive().optional(),
|
|
408
|
+
})
|
|
409
|
+
.passthrough()
|
|
410
|
+
.parse(args ?? {});
|
|
411
|
+
const warnings = [];
|
|
412
|
+
const payload = normalizePageSize(parsed, warnings);
|
|
413
|
+
const result = await client.post("/integration/agent/manual/search", payload);
|
|
414
|
+
return formatToolResult(result, warnings);
|
|
415
|
+
});
|
|
416
|
+
});
|
|
417
|
+
server.registerTool("mes_manual.get", {
|
|
418
|
+
title: "Get MES operation manual section",
|
|
419
|
+
description: "按手册章节 ID 获取后端提供的 MES AI 操作手册详情,返回动作链、字段归属、步骤和常见误用。",
|
|
420
|
+
inputSchema: z.object({
|
|
421
|
+
id: z.string().min(1).describe("手册章节 ID"),
|
|
422
|
+
}),
|
|
423
|
+
annotations: {
|
|
424
|
+
readOnlyHint: true,
|
|
425
|
+
destructiveHint: false,
|
|
426
|
+
idempotentHint: true,
|
|
427
|
+
},
|
|
428
|
+
}, async (args) => {
|
|
429
|
+
return runTool({ toolName: "mes_manual.get" }, async () => {
|
|
430
|
+
const parsed = z
|
|
431
|
+
.object({
|
|
432
|
+
id: z.string().min(1),
|
|
433
|
+
})
|
|
434
|
+
.parse(args ?? {});
|
|
435
|
+
const result = await client.post("/integration/agent/manual/detail", parsed);
|
|
436
|
+
return formatToolResult(result);
|
|
437
|
+
});
|
|
438
|
+
});
|
|
439
|
+
server.registerTool("mes_workflow.detail", {
|
|
440
|
+
title: "Get MES recommended workflow",
|
|
441
|
+
description: "按工作流 ID 获取后端推荐的 MES 业务动作链。用于需要多个动作配合完成的场景,例如先更新生产计划开始日期,再从生产计划排产。",
|
|
442
|
+
inputSchema: z.object({
|
|
443
|
+
id: z.string().min(1).describe("业务工作流 ID"),
|
|
444
|
+
}),
|
|
445
|
+
annotations: {
|
|
446
|
+
readOnlyHint: true,
|
|
447
|
+
destructiveHint: false,
|
|
448
|
+
idempotentHint: true,
|
|
449
|
+
},
|
|
450
|
+
}, async (args) => {
|
|
451
|
+
return runTool({ toolName: "mes_workflow.detail" }, async () => {
|
|
452
|
+
const parsed = z
|
|
453
|
+
.object({
|
|
454
|
+
id: z.string().min(1),
|
|
455
|
+
})
|
|
456
|
+
.parse(args ?? {});
|
|
457
|
+
const result = await client.post("/integration/agent/workflows/detail", parsed);
|
|
458
|
+
return formatToolResult(result);
|
|
459
|
+
});
|
|
460
|
+
});
|
|
291
461
|
server.registerTool("mes_action.execute", {
|
|
292
462
|
title: "Execute MES business action",
|
|
293
463
|
description: "执行一个由 mes_action.list 发现的 MES 页面业务动作。后端复用页面 Controller、DTO、权限、租户、状态机和调用日志。返回体是执行信封:真实业务结果在 data 字段;confirm_required 返回待确认 invocationId,必须由同一公司/租户下有权限的账号审批。",
|